Action refactor

This commit is contained in:
Hetu Nandu 2020-02-18 10:41:52 +00:00
parent b22478e96e
commit fb80c4b576
63 changed files with 1721 additions and 1200 deletions

View File

@ -46,6 +46,7 @@
"husky": "^3.0.5",
"interweave": "^12.1.1",
"interweave-autolink": "^4.0.1",
"json-fn": "^1.1.1",
"lint-staged": "^9.2.5",
"localforage": "^1.7.3",
"lodash": "^4.17.11",
@ -74,6 +75,7 @@
"react-select": "^3.0.8",
"react-spring": "^8.0.27",
"react-tabs": "^3.0.0",
"react-toastify": "^5.5.0",
"react-transition-group": "^4.3.0",
"redux": "^4.0.1",
"redux-form": "^8.2.6",

View File

@ -1,4 +1,4 @@
import { RestAction, PaginationField } from "api/ActionAPI";
import { RestAction, PaginationField, ActionResponse } from "api/ActionAPI";
import {
ReduxActionTypes,
ReduxAction,
@ -130,6 +130,19 @@ export const copyActionError = (payload: {
};
};
export const executeApiActionRequest = (payload: { id: string }) => ({
type: ReduxActionTypes.EXECUTE_API_ACTION_REQUEST,
payload: payload,
});
export const executeApiActionSuccess = (payload: {
id: string;
response: ActionResponse;
}) => ({
type: ReduxActionTypes.EXECUTE_API_ACTION_SUCCESS,
payload: payload,
});
export default {
createAction: createActionRequest,
fetchActions,

View File

@ -24,4 +24,5 @@ export interface UpdateWidgetPropertyPayload {
propertyValue: any;
renderMode: RenderMode;
dynamicBindings?: Record<string, boolean>;
dynamicTriggers?: Record<string, true>;
}

View File

@ -3,27 +3,18 @@ import {
ReduxAction,
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants";
import { PaginationField } from "api/ActionAPI";
import {
ActionPayload,
ExecuteActionPayload,
ExecuteErrorPayload,
PageAction,
} from "constants/ActionConstants";
export const executeAction = (
actionPayloads: ActionPayload[],
paginationField?: PaginationField,
): ReduxAction<{
actions: ActionPayload[];
paginationField: PaginationField;
}> => {
payload: ExecuteActionPayload,
): ReduxAction<ExecuteActionPayload> => {
return {
type: ReduxActionTypes.EXECUTE_ACTION,
payload: {
actions: actionPayloads,
paginationField: paginationField,
},
payload,
};
};

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.00033 0.666656C7.35215 0.666656 5.74099 1.1554 4.37058 2.07108C3.00017 2.98675 1.93206 4.28824 1.30133 5.81096C0.670603 7.33368 0.505575 9.00923 0.827119 10.6257C1.14866 12.2423 1.94234 13.7271 3.10777 14.8925C4.27321 16.058 5.75807 16.8517 7.37458 17.1732C8.99109 17.4947 10.6666 17.3297 12.1894 16.699C13.7121 16.0683 15.0136 15.0002 15.9292 13.6297C16.8449 12.2593 17.3337 10.6482 17.3337 8.99999C17.3337 7.90564 17.1181 6.82201 16.6993 5.81096C16.2805 4.79991 15.6667 3.88125 14.8929 3.10743C14.1191 2.33361 13.2004 1.71978 12.1894 1.30099C11.1783 0.882205 10.0947 0.666656 9.00033 0.666656ZM11.2587 10.075C11.3368 10.1525 11.3988 10.2446 11.4411 10.3462C11.4834 10.4477 11.5052 10.5566 11.5052 10.6667C11.5052 10.7767 11.4834 10.8856 11.4411 10.9871C11.3988 11.0887 11.3368 11.1809 11.2587 11.2583C11.1812 11.3364 11.089 11.3984 10.9875 11.4407C10.8859 11.483 10.777 11.5048 10.667 11.5048C10.557 11.5048 10.4481 11.483 10.3465 11.4407C10.245 11.3984 10.1528 11.3364 10.0753 11.2583L9.00033 10.175L7.92533 11.2583C7.84786 11.3364 7.75569 11.3984 7.65414 11.4407C7.55259 11.483 7.44367 11.5048 7.33366 11.5048C7.22365 11.5048 7.11473 11.483 7.01318 11.4407C6.91163 11.3984 6.81946 11.3364 6.742 11.2583C6.66389 11.1809 6.60189 11.0887 6.55959 10.9871C6.51728 10.8856 6.4955 10.7767 6.4955 10.6667C6.4955 10.5566 6.51728 10.4477 6.55959 10.3462C6.60189 10.2446 6.66389 10.1525 6.742 10.075L7.82533 8.99999L6.742 7.92499C6.58508 7.76807 6.49692 7.55524 6.49692 7.33332C6.49692 7.11141 6.58508 6.89858 6.742 6.74166C6.89892 6.58474 7.11174 6.49658 7.33366 6.49658C7.55558 6.49658 7.76841 6.58474 7.92533 6.74166L9.00033 7.82499L10.0753 6.74166C10.2322 6.58474 10.4451 6.49658 10.667 6.49658C10.8889 6.49658 11.1017 6.58474 11.2587 6.74166C11.4156 6.89858 11.5037 7.11141 11.5037 7.33332C11.5037 7.55524 11.4156 7.76807 11.2587 7.92499L10.1753 8.99999L11.2587 10.075Z" fill="#CE4257"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.00131 0.666687C7.35313 0.666687 5.74196 1.15543 4.37155 2.07111C3.00114 2.98679 1.93304 4.28827 1.30231 5.81099C0.671579 7.33371 0.506552 9.00926 0.828095 10.6258C1.14964 12.2423 1.94331 13.7271 3.10875 14.8926C4.27419 16.058 5.75905 16.8517 7.37555 17.1732C8.99206 17.4948 10.6676 17.3297 12.1903 16.699C13.7131 16.0683 15.0145 15.0002 15.9302 13.6298C16.8459 12.2594 17.3346 10.6482 17.3346 9.00002C17.3346 7.90567 17.1191 6.82204 16.7003 5.81099C16.2815 4.79994 15.6677 3.88129 14.8939 3.10746C14.12 2.33364 13.2014 1.71981 12.1903 1.30102C11.1793 0.882235 10.0957 0.666687 9.00131 0.666687ZM9.83464 12.3334C9.83464 12.5544 9.74684 12.7663 9.59056 12.9226C9.43428 13.0789 9.22232 13.1667 9.00131 13.1667C8.78029 13.1667 8.56833 13.0789 8.41205 12.9226C8.25577 12.7663 8.16797 12.5544 8.16797 12.3334V8.16669C8.16797 7.94567 8.25577 7.73371 8.41205 7.57743C8.56833 7.42115 8.78029 7.33335 9.00131 7.33335C9.22232 7.33335 9.43428 7.42115 9.59056 7.57743C9.74684 7.73371 9.83464 7.94567 9.83464 8.16669V12.3334ZM9.00131 6.50002C8.83649 6.50002 8.67537 6.45115 8.53833 6.35958C8.40129 6.26801 8.29448 6.13786 8.23141 5.98559C8.16833 5.83332 8.15183 5.66576 8.18398 5.50411C8.21614 5.34246 8.29551 5.19398 8.41205 5.07743C8.52859 4.96089 8.67708 4.88152 8.83873 4.84937C9.00038 4.81721 9.16794 4.83371 9.32021 4.89679C9.47248 4.95986 9.60263 5.06667 9.6942 5.20371C9.78576 5.34075 9.83464 5.50187 9.83464 5.66669C9.83464 5.8877 9.74684 6.09966 9.59056 6.25594C9.43428 6.41222 9.22232 6.50002 9.00131 6.50002Z" fill="#0384FE"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.00033 0.666687C7.35215 0.666687 5.74099 1.15543 4.37058 2.07111C3.00017 2.98679 1.93206 4.28827 1.30133 5.81099C0.670603 7.33371 0.505575 9.00926 0.827119 10.6258C1.14866 12.2423 1.94234 13.7271 3.10777 14.8926C4.27321 16.058 5.75807 16.8517 7.37458 17.1732C8.99109 17.4948 10.6666 17.3297 12.1894 16.699C13.7121 16.0683 15.0136 15.0002 15.9292 13.6298C16.8449 12.2594 17.3337 10.6482 17.3337 9.00002C17.3337 7.90567 17.1181 6.82204 16.6993 5.81099C16.2805 4.79994 15.6667 3.88129 14.8929 3.10746C14.1191 2.33364 13.2004 1.71981 12.1894 1.30102C11.1783 0.882235 10.0947 0.666687 9.00033 0.666687ZM12.5837 7.00835L8.77533 12.0084C8.6977 12.1092 8.598 12.1909 8.48388 12.2473C8.36977 12.3036 8.24426 12.333 8.117 12.3334C7.99042 12.334 7.86535 12.3059 7.75128 12.251C7.63721 12.1961 7.53714 12.116 7.45866 12.0167L5.42533 9.42502C5.35803 9.33857 5.30841 9.2397 5.27932 9.13408C5.25022 9.02845 5.24222 8.91812 5.25576 8.8094C5.2693 8.70068 5.30412 8.59569 5.35824 8.50042C5.41236 8.40516 5.48471 8.32149 5.57116 8.25419C5.74576 8.11826 5.96721 8.05727 6.18678 8.08462C6.2955 8.09816 6.40049 8.13298 6.49576 8.1871C6.59102 8.24122 6.67469 8.31357 6.742 8.40002L8.10033 10.1334L11.2503 5.96669C11.3171 5.87914 11.4004 5.8056 11.4956 5.75026C11.5908 5.69492 11.6959 5.65887 11.805 5.64417C11.9141 5.62947 12.0251 5.6364 12.1315 5.66457C12.2379 5.69274 12.3378 5.7416 12.4253 5.80835C12.5129 5.87511 12.5864 5.95846 12.6418 6.05363C12.6971 6.14881 12.7331 6.25395 12.7478 6.36306C12.7625 6.47217 12.7556 6.58311 12.7274 6.68954C12.6993 6.79597 12.6504 6.89581 12.5837 6.98335V7.00835Z" fill="#36AB80"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,3 @@
<svg width="20" height="17" viewBox="0 0 20 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.7999 12.5833L12.4083 1.98331C12.1498 1.57899 11.7937 1.24625 11.3728 1.01576C10.9519 0.785273 10.4798 0.664459 9.99992 0.664459C9.52005 0.664459 9.04791 0.785273 8.62702 1.01576C8.20613 1.24625 7.85004 1.57899 7.59159 1.98331L1.19992 12.5833C0.974132 12.9597 0.851411 13.3889 0.844097 13.8277C0.836783 14.2666 0.945133 14.6996 1.15825 15.0833C1.40465 15.5152 1.7613 15.8739 2.19176 16.1228C2.62221 16.3717 3.11102 16.5019 3.60825 16.5H16.3916C16.8855 16.5052 17.3722 16.3801 17.8023 16.1373C18.2325 15.8944 18.591 15.5423 18.8416 15.1166C19.061 14.729 19.1728 14.2897 19.1655 13.8443C19.1581 13.3989 19.0319 12.9636 18.7999 12.5833ZM9.99992 13.1666C9.8351 13.1666 9.67399 13.1178 9.53694 13.0262C9.3999 12.9346 9.29309 12.8045 9.23002 12.6522C9.16695 12.4999 9.15044 12.3324 9.1826 12.1707C9.21475 12.0091 9.29412 11.8606 9.41066 11.7441C9.52721 11.6275 9.67569 11.5481 9.83734 11.516C9.999 11.4838 10.1666 11.5003 10.3188 11.5634C10.4711 11.6265 10.6012 11.7333 10.6928 11.8703C10.7844 12.0074 10.8333 12.1685 10.8333 12.3333C10.8333 12.5543 10.7455 12.7663 10.5892 12.9226C10.4329 13.0788 10.2209 13.1666 9.99992 13.1666ZM10.8333 9.83331C10.8333 10.0543 10.7455 10.2663 10.5892 10.4226C10.4329 10.5788 10.2209 10.6666 9.99992 10.6666C9.77891 10.6666 9.56695 10.5788 9.41066 10.4226C9.25438 10.2663 9.16659 10.0543 9.16659 9.83331V6.49998C9.16659 6.27896 9.25438 6.067 9.41066 5.91072C9.56695 5.75444 9.77891 5.66665 9.99992 5.66665C10.2209 5.66665 10.4329 5.75444 10.5892 5.91072C10.7455 6.067 10.8333 6.27896 10.8333 6.49998V9.83331Z" fill="#F69D2C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -139,7 +139,7 @@ const mapButtonStyleToStyleName = (buttonStyle?: ButtonStyle) => {
const ButtonContainer = (props: ButtonContainerProps & ButtonStyleProps) => {
return (
<BaseButton
className={props.isLoading ? "bp3-skeleton" : ""}
loading={props.isLoading}
icon={props.icon}
rightIcon={props.rightIcon}
text={props.text}

View File

@ -107,7 +107,7 @@ const DropdownContainer = styled.div`
border-radius: ${props => props.theme.radii[1]}px;
box-shadow: 0px 2px 4px rgba(67, 70, 74, 0.14);
padding: ${props => props.theme.spaces[3]}px;
background:white;
background: white;
}
&& .${Classes.POPOVER_WRAPPER} {
@ -123,7 +123,6 @@ const DropdownContainer = styled.div`
max-width: 100%;
max-height: auto;
}
width: 100%;
`;

View File

@ -20,7 +20,6 @@ import {
import React, { useRef, MutableRefObject, useEffect, memo } from "react";
import styled from "constants/DefaultTheme";
import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl";
import { ActionPayload } from "constants/ActionConstants";
import { Classes } from "@blueprintjs/core";
import { TablePagination } from "../appsmith/TablePagination";
@ -32,7 +31,7 @@ export interface TableComponentProps {
height: number;
width: number;
columnActions?: ColumnAction[];
onCommandClick: (actions: ActionPayload[]) => void;
onCommandClick: (dynamicTrigger: string) => void;
disableDrag: (disable: boolean) => void;
nextPageClick: Function;
prevPageClick: Function;
@ -158,7 +157,7 @@ const TableComponent = memo(
const commands: CommandModel[] = (props.columnActions || []).map(action => {
return {
buttonOption: { content: action.label },
data: action.actionPayloads,
data: action.dynamicTrigger,
};
});
@ -172,7 +171,7 @@ const TableComponent = memo(
action.label.toLowerCase() === _target.title.toLowerCase(),
)
.forEach(action => {
props.onCommandClick(action.actionPayloads);
props.onCommandClick(action.dynamicTrigger);
});
}
}

View File

@ -0,0 +1,445 @@
import React, { ChangeEvent } from "react";
import { connect } from "react-redux";
import { AppState } from "reducers";
import { DropdownOption } from "widgets/DropdownWidget";
import _ from "lodash";
import { ControlWrapper } from "components/propertyControls/StyledControls";
import { InputText } from "components/propertyControls/InputTextControl";
import StyledDropdown from "components/editorComponents/StyledDropdown";
const ACTION_TRIGGER_REGEX = /^{{([\s\S]*?)\(([\s\S]*?)\)}}$/g;
const ACTION_ANONYMOUS_FUNC_REGEX = /\(\) => ([\s\S]*?)(\([\s\S]*?\))/g;
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" },
];
type ValueChangeHandler = (changeValue: string, currentValue: string) => string;
type ActionCreatorArgumentConfig = {
label: string;
field: string;
valueChangeHandler: ValueChangeHandler;
getSelectedValue: (value: string, returnArguments: boolean) => string;
};
interface ActionCreatorDropdownOption extends DropdownOption {
arguments: ActionCreatorArgumentConfig[];
}
const handleTopLevelFuncUpdate: ValueChangeHandler = (
value: string,
): string => {
return value === "none" ? "" : `{{${value}()}}`;
};
const handleApiArgSelect = (
changeValue: string,
currentValue: string,
label: "onSuccess" | "onError",
): string => {
const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)];
const args = [...matches[0][2].matchAll(ACTION_ANONYMOUS_FUNC_REGEX)];
let successArg = args[0] ? args[0][0] : "() => {}";
let errorArg = args[1] ? args[1][0] : "() => {}";
if (label === "onSuccess") {
successArg = changeValue.endsWith(")")
? `() => ${changeValue}`
: `() => ${changeValue}()`;
}
if (label === "onError") {
errorArg = changeValue.endsWith(")")
? `() => ${changeValue}`
: `() => ${changeValue}()`;
}
return currentValue.replace(
ACTION_TRIGGER_REGEX,
`{{$1(${successArg}, ${errorArg})}}`,
);
};
const handlePageNameArgSelect = (changeValue: string, currentValue: string) => {
return currentValue.replace(ACTION_TRIGGER_REGEX, `{{$1(${changeValue})}}`);
};
const handleTextArgChange = (
changeValue: string,
currentValue: string,
): string => {
return currentValue.replace(ACTION_TRIGGER_REGEX, `{{$1('${changeValue}')}}`);
};
const handleAlertTextChange = (
changeValue: string,
currentValue: string,
): string => {
const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)];
const args = matches[0][2].split(",");
args[0] = `'${changeValue}'`;
return currentValue.replace(
ACTION_TRIGGER_REGEX,
`{{$1(${args.join(",")})}}`,
);
};
const handleAlertTypeChange = (
changeValue: string,
currentValue: string,
): string => {
const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)];
const args = matches[0][2].split(",");
args[1] = changeValue;
return currentValue.replace(
ACTION_TRIGGER_REGEX,
`{{$1(${args.join(",")})}}`,
);
};
const getApiArgumentValue = (
value: string,
label: "onSuccess" | "onError",
returnSubArguments = false,
): string => {
let selectedValue = "none";
let selectedValueArgs = "";
const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
if (matches.length) {
const funcArgs = matches[0][2];
const argIndex = label === "onSuccess" ? 0 : 1;
const args = [...funcArgs.matchAll(ACTION_ANONYMOUS_FUNC_REGEX)];
const selectedArg = args[argIndex];
if (selectedArg && selectedArg.length) {
selectedValue = selectedArg[1];
selectedValueArgs = selectedArg[2];
}
}
if (returnSubArguments) return selectedValueArgs;
return selectedValue;
};
const getPageNameSelectedValue = (value: string) => {
const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
return matches.length ? matches[0][2] : "none";
};
const getTextArgValue = (value: string) => {
const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
if (matches.length) {
const stringValue = matches[0][2];
return stringValue.substring(1, stringValue.length - 1);
}
return "";
};
const getAlertTextValue = (value: string) => {
const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
if (matches.length) {
const funcArgs = matches[0][2];
const arg = funcArgs.split(",")[0];
return arg.substring(1, arg.length - 1);
}
return "";
};
const getAlertTypeValue = (value: string) => {
const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)];
if (matches.length) {
const funcArgs = matches[0][2];
const arg = funcArgs.split(",")[1];
return arg ? arg.trim() : "'primary'";
}
return "";
};
export const PropertyPaneActionDropdownOptions: ActionCreatorDropdownOption[] = [
{
label: "No action",
value: "none",
id: "none",
arguments: [],
},
{
label: "Call API",
value: "api",
id: "api",
arguments: [
{
label: "onSuccess",
field: "ACTION_SELECTOR_FIELD",
valueChangeHandler: (changeValue, currentValue) =>
handleApiArgSelect(changeValue, currentValue, "onSuccess"),
getSelectedValue: (value: string, returnArgs = false) =>
getApiArgumentValue(value, "onSuccess", returnArgs),
},
{
label: "onError",
field: "ACTION_SELECTOR_FIELD",
valueChangeHandler: (changeValue, currentValue) =>
handleApiArgSelect(changeValue, currentValue, "onError"),
getSelectedValue: (value: string, returnArgs = false) =>
getApiArgumentValue(value, "onError", returnArgs),
},
],
},
{
label: "Navigate to Page",
value: "navigateTo",
id: "navigateTo",
arguments: [
{
label: "pageName",
field: "PAGE_SELECTOR_FIELD",
valueChangeHandler: handlePageNameArgSelect,
getSelectedValue: getPageNameSelectedValue,
},
],
},
{
label: "Navigate to URL",
value: "navigateToUrl",
id: "navigateToUrl",
arguments: [
{
label: "URL",
field: "TEXT_FIELD",
valueChangeHandler: handleTextArgChange,
getSelectedValue: getTextArgValue,
},
],
},
{
label: "Show Alert",
value: "showAlert",
id: "showAlert",
arguments: [
{
label: "text",
field: "TEXT_FIELD",
valueChangeHandler: handleAlertTextChange,
getSelectedValue: getAlertTextValue,
},
{
label: "type",
field: "ALERT_TYPE_SELECTOR_FIELD",
valueChangeHandler: handleAlertTypeChange,
getSelectedValue: getAlertTypeValue,
},
],
},
];
type ReduxStateProps = {
actions: DropdownOption[];
pageNameDropdown: DropdownOption[];
};
interface Props {
value: string;
onValueChange: (newValue: string) => void;
}
class DynamicActionCreator extends React.Component<Props & ReduxStateProps> {
getTopLevelFuncValue = (value: string) => {
let matches: any[] = [];
if (value) {
matches = value ? [...value.matchAll(ACTION_TRIGGER_REGEX)] : [];
}
let mainFuncSelectedValue = "none";
if (matches.length) {
mainFuncSelectedValue = matches[0][1] || "none";
}
return mainFuncSelectedValue;
};
handleValueUpdate = (
updateValueOrEvent: string | ChangeEvent<HTMLTextAreaElement>,
valueUpdateHandler: ValueChangeHandler,
) => {
const { value, onValueChange } = this.props;
let updateValue = updateValueOrEvent;
if (typeof updateValueOrEvent !== "string") {
updateValue = updateValueOrEvent.target.value;
}
const newValue = valueUpdateHandler(updateValue as string, value);
onValueChange(newValue);
};
renderSubArgumentFields = (
argValue: string,
allOptions: ActionCreatorDropdownOption[],
parentChangeHandler: (
updateValueOrEvent: string | ChangeEvent<HTMLTextAreaElement>,
valueUpdateHandler: ValueChangeHandler,
) => void,
argumentConfig: ActionCreatorArgumentConfig,
) => {
const subArgValue = argumentConfig.getSelectedValue(argValue, false);
const subArguments = argumentConfig.getSelectedValue(argValue, true);
let selectedOption = allOptions[0];
allOptions
.filter(o => o.value !== "api")
.forEach(o => {
if (o.value === subArgValue) {
selectedOption = o;
}
});
const handleValueUpdate = (
updateValueOrEvent: string | ChangeEvent<HTMLTextAreaElement>,
valueUpdateHandler: ValueChangeHandler,
) => {
let updateValue = updateValueOrEvent;
if (typeof updateValueOrEvent !== "string") {
updateValue = updateValueOrEvent.target.value;
}
const tempArg = `{{${subArgValue}${subArguments}}}`;
const newValue = valueUpdateHandler(updateValue as string, tempArg);
const newArgValue = newValue.substring(2, newValue.length - 2);
parentChangeHandler(newArgValue, argumentConfig.valueChangeHandler);
};
const subFunctionCall = `{{${subArgValue}${subArguments}}}`;
return this.renderActionArgumentFields(
subFunctionCall,
selectedOption,
allOptions,
handleValueUpdate,
);
};
renderActionArgumentFields = (
value: string,
selectedOption: ActionCreatorDropdownOption,
allOptions: ActionCreatorDropdownOption[],
handleUpdate: (
updateValueOrEvent: string | ChangeEvent<HTMLTextAreaElement>,
valueUpdateHandler: ValueChangeHandler,
) => void,
) => {
return (
<div style={{ paddingLeft: 5 }}>
{selectedOption.arguments.map(arg => {
switch (arg.field) {
case "ACTION_SELECTOR_FIELD":
return (
<ControlWrapper key={arg.label}>
<label>{arg.label}</label>
<StyledDropdown
options={allOptions}
selectedValue={arg.getSelectedValue(value, false)}
onSelect={value =>
handleUpdate(value, arg.valueChangeHandler)
}
/>
{this.renderSubArgumentFields(
value,
allOptions,
handleUpdate,
arg,
)}
</ControlWrapper>
);
case "PAGE_SELECTOR_FIELD":
return (
<ControlWrapper key={arg.label}>
<label>{arg.label}</label>
<StyledDropdown
options={this.props.pageNameDropdown}
selectedValue={arg.getSelectedValue(value, false)}
onSelect={value =>
handleUpdate(value, arg.valueChangeHandler)
}
/>
</ControlWrapper>
);
case "TEXT_FIELD":
return (
<React.Fragment key={arg.label}>
<InputText
label={arg.label}
value={arg.getSelectedValue(value, false)}
onChange={e => handleUpdate(e, arg.valueChangeHandler)}
isValid={true}
/>
</React.Fragment>
);
case "ALERT_TYPE_SELECTOR_FIELD":
return (
<ControlWrapper key={arg.label}>
<label>{arg.label}</label>
<StyledDropdown
options={ALERT_STYLE_OPTIONS}
selectedValue={arg.getSelectedValue(value, false)}
onSelect={value =>
handleUpdate(value, arg.valueChangeHandler)
}
/>
</ControlWrapper>
);
default:
return null;
}
})}
</div>
);
};
render() {
const { actions, value } = this.props;
const topLevelFuncValue = this.getTopLevelFuncValue(value);
const actionOptions = PropertyPaneActionDropdownOptions.map(o => {
if (o.id === "api") {
return {
...o,
children: actions.map(a => ({ ...o, ...a })),
};
} else {
return o;
}
});
let selectedOption = actionOptions[0];
actionOptions.forEach(o => {
if (
o.value === topLevelFuncValue ||
_.some(o.children, {
value: topLevelFuncValue,
})
) {
selectedOption = o;
}
});
return (
<React.Fragment>
<StyledDropdown
options={actionOptions}
selectedValue={topLevelFuncValue}
onSelect={value =>
this.handleValueUpdate(value, handleTopLevelFuncUpdate)
}
/>
{this.renderActionArgumentFields(
value,
selectedOption,
actionOptions,
this.handleValueUpdate,
)}
</React.Fragment>
);
}
}
const mapStateToProps = (state: AppState): ReduxStateProps => ({
actions: state.entities.actions.map(a => ({
label: a.config.name,
id: a.config.id,
value: `${a.config.name}.run`,
})),
pageNameDropdown: state.entities.pageList.pages.map(p => ({
label: p.pageName,
id: p.pageId,
value: `'${p.pageName}'`,
})),
});
export default connect(mapStateToProps)(DynamicActionCreator);

View File

@ -10,15 +10,13 @@ import "codemirror/addon/hint/javascript-hint";
import "codemirror/addon/display/placeholder";
import "codemirror/addon/edit/closebrackets";
import "codemirror/addon/display/autorefresh";
import {
getNameBindingsForAutocomplete,
NameBindingsWithData,
} from "selectors/nameBindingsWithDataSelector";
import { getDataTreeForAutocomplete } from "selectors/dataTreeSelectors";
import { AUTOCOMPLETE_MATCH_REGEX } from "constants/BindingsConstants";
import ErrorTooltip from "components/editorComponents/ErrorTooltip";
import { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form";
import _ from "lodash";
import { parseDynamicString } from "utils/DynamicBindingUtils";
import { DataTree } from "entities/DataTree/dataTreeFactory";
require("codemirror/mode/javascript/javascript");
const HintStyles = createGlobalStyle`
@ -125,7 +123,7 @@ const THEMES = {
type THEME = "LIGHT" | "DARK";
interface ReduxStateProps {
dynamicData: NameBindingsWithData;
dynamicData: DataTree;
}
export type DynamicAutocompleteInputProps = {
@ -306,7 +304,7 @@ class DynamicAutocompleteInput extends Component<Props, State> {
}
const mapStateToProps = (state: AppState): ReduxStateProps => ({
dynamicData: getNameBindingsForAutocomplete(state),
dynamicData: getDataTreeForAutocomplete(state),
});
export default connect(mapStateToProps)(DynamicAutocompleteInput);

View File

@ -9,19 +9,15 @@ import { updateWidget } from "actions/pageActions";
import { executeAction, disableDragAction } from "actions/widgetActions";
import { updateWidgetProperty } from "actions/controlActions";
import { ActionPayload } from "constants/ActionConstants";
import { ExecuteActionPayload } from "constants/ActionConstants";
import { RenderModes } from "constants/WidgetConstants";
import { OccupiedSpace } from "constants/editorConstants";
import { getOccupiedSpaces } from "selectors/editorSelectors";
import { PaginationField } from "api/ActionAPI";
import { updateWidgetMetaProperty } from "actions/metaActions";
export type EditorContextType = {
executeAction?: (
actionPayloads: ActionPayload[],
paginationField?: PaginationField,
) => void;
executeAction?: (actionPayloads: ExecuteActionPayload) => void;
updateWidget?: (
operation: WidgetOperation,
widgetId: string,
@ -99,21 +95,19 @@ const mapDispatchToProps = (dispatch: any) => {
RenderModes.CANVAS,
),
),
executeAction: (actionPayload: ExecuteActionPayload) =>
dispatch(executeAction(actionPayload)),
updateWidget: (
operation: WidgetOperation,
widgetId: string,
payload: any,
) => dispatch(updateWidget(operation, widgetId, payload)),
updateWidgetMetaProperty: (
widgetId: string,
propertyName: string,
propertyValue: any,
) =>
dispatch(updateWidgetMetaProperty(widgetId, propertyName, propertyValue)),
executeAction: (
actionPayloads: ActionPayload[],
paginationField?: PaginationField,
) => dispatch(executeAction(actionPayloads, paginationField)),
updateWidget: (
operation: WidgetOperation,
widgetId: string,
payload: any,
) => dispatch(updateWidget(operation, widgetId, payload)),
disableDrag: (disable: boolean) => {
dispatch(disableDragAction(disable));
},

View File

@ -0,0 +1,101 @@
import React from "react";
import _ from "lodash";
import { DropdownOption } from "widgets/DropdownWidget";
import {
StyledDropDown,
StyledDropDownContainer,
} from "components/propertyControls/StyledControls";
import {
Button,
MenuItem,
PopoverInteractionKind,
PopoverPosition,
} from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
type ActionTypeDropdownProps = {
options: DropdownOption[];
selectedValue: string;
onSelect: (value: string) => void;
};
class StyledDropdown extends React.Component<ActionTypeDropdownProps> {
handleSelect = (option: DropdownOption) => {
this.props.onSelect(option.value);
};
renderItem = (option: DropdownOption) => {
const isSelected = this.isOptionSelected(option);
return (
<MenuItem
className="single-select"
active={isSelected}
key={option.value}
onClick={() => this.handleSelect(option)}
text={option.label}
popoverProps={{
minimal: true,
hoverCloseDelay: 0,
interactionKind: PopoverInteractionKind.HOVER,
position: PopoverPosition.BOTTOM,
modifiers: {
arrow: {
enabled: false,
},
offset: {
enabled: true,
offset: "-16px, 0",
},
},
}}
>
{option.children && option.children.map(this.renderItem)}
</MenuItem>
);
};
isOptionSelected = (currentOption: DropdownOption) => {
if (currentOption.children) {
return _.some(currentOption.children, {
value: this.props.selectedValue,
});
} else {
return currentOption.value === this.props.selectedValue;
}
};
render() {
const { selectedValue } = this.props;
let selectedOption = this.props.options[0];
this.props.options.forEach(o => {
if (o.value === selectedValue) {
selectedOption = o;
} else {
const childOption = _.find(o.children, {
value: this.props.selectedValue,
});
if (childOption) selectedOption = childOption;
}
});
return (
<StyledDropDownContainer>
<StyledDropDown
filterable={false}
items={this.props.options}
itemRenderer={this.renderItem}
onItemSelect={_.noop}
popoverProps={{
minimal: true,
usePortal: false,
position: PopoverPosition.BOTTOM,
}}
>
<Button
rightIcon={IconNames.CHEVRON_DOWN}
text={selectedOption.label}
/>
</StyledDropDown>
</StyledDropDownContainer>
);
}
}
export default StyledDropdown;

View File

@ -1,7 +1,52 @@
import { Position, Toaster } from "@blueprintjs/core";
import React from "react";
import { toast, ToastOptions, TypeOptions, ToastType } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import styled from "styled-components";
import { theme } from "constants/DefaultTheme";
import { AlertIcons } from "icons/AlertIcons";
// To add a toast import this instance and call .show()
// https://blueprintjs.com/docs/#core/components/toast.example
export const AppToaster = Toaster.create({
position: Position.BOTTOM_RIGHT,
});
const ToastBody = styled.div<{ type: TypeOptions }>`
height: 100%;
border-left: 4px solid ${({ type }) => theme.alert[type].color};
border-radius: 4px;
background-color: white;
color: black;
display: flex;
align-items: center;
padding-left: 5px;
`;
const ToastMessage = styled.span`
font-size: 16px;
margin: 0 5px;
`;
const ToastIcon = {
info: AlertIcons.INFO,
success: AlertIcons.SUCCESS,
error: AlertIcons.ERROR,
warning: AlertIcons.WARNING,
default: AlertIcons.INFO,
};
type Props = ToastOptions & { message: string; closeToast?: () => void };
const ToastComponent = (props: Props) => {
const alertType = props.type || ToastType.INFO;
const Icon = ToastIcon[alertType];
return (
<ToastBody type={alertType}>
<Icon color={theme.alert[alertType].color} width={20} height={20} />
<ToastMessage>{props.message}</ToastMessage>
</ToastBody>
);
};
const Toaster = {
show: (config: ToastOptions & { message: string }) => {
toast(<ToastComponent {...config} />);
},
clear: () => toast.dismiss(),
};
export const AppToaster = Toaster;

View File

@ -1,336 +0,0 @@
import React from "react";
import { ActionPayload, ActionType } from "constants/ActionConstants";
import { DropdownOption } from "widgets/DropdownWidget";
import { MenuItem, Button } from "@blueprintjs/core";
import styled, { theme } from "constants/DefaultTheme";
import { CloseButton } from "components/designSystems/blueprint/CloseButton";
import { StyledDropDown } from "./StyledControls";
import { IItemRendererProps } from "@blueprintjs/select";
import { InputText } from "./InputTextControl";
const DEFAULT_ACTION_TYPE = "Select Action Type" as ActionType;
const DEFAULT_ACTION_LABEL = "Select Action";
enum ACTION_RESOLUTION_TYPE {
SUCCESS,
ERROR,
}
const renderItem = (option: DropdownOption, itemProps: IItemRendererProps) => {
if (!itemProps.modifiers.matchesPredicate) {
return null;
}
return (
<MenuItem
active={itemProps.modifiers.active}
key={option.value}
onClick={itemProps.handleClick}
text={option.label}
/>
);
};
const ActionSelectorDropDown = styled(StyledDropDown)`
&&&&& {
display: block;
margin-bottom: 2px;
width: 100%;
.bp3-popover-target {
width: 100%;
.bp3-button {
justify-content: flex-start;
width: 100%;
.bp3-icon {
order: 2;
margin-left: auto;
}
}
}
}
`;
const ActionSelectorDropDownContainer = styled.div`
position: relative;
`;
interface ActionSelectorProps {
allActions: DropdownOption[];
actionTypeOptions: DropdownOption[];
selectedActionType: ActionType;
selectedActionLabel: string;
label: string;
identifier: string;
labelEditable?: boolean;
actionResolutionType: ACTION_RESOLUTION_TYPE | string;
updateActions: (actions: ActionPayload[], key?: string) => void;
updateLabel?: (label: string, key: string) => void;
actions: ActionPayload[];
}
export default function ActionSelector(props: ActionSelectorProps) {
function clearActionSelectorType(
actionResolutionType: ACTION_RESOLUTION_TYPE | string,
) {
let actionPayloads: ActionPayload[] = props.actions
? props.actions.slice()
: [];
const actionPayload = actionPayloads[0];
switch (actionResolutionType) {
case props.identifier:
actionPayloads = [];
break;
case ACTION_RESOLUTION_TYPE.SUCCESS:
actionPayload.onSuccess = undefined;
break;
case ACTION_RESOLUTION_TYPE.ERROR:
actionPayload.onError = undefined;
break;
}
props.updateActions(actionPayloads, props.identifier);
}
function clearActionSelectorLabel(
actionResolutionType: ACTION_RESOLUTION_TYPE | string,
) {
let actionPayloads = props.actions.slice();
const actionPayload = (props.actions[0] as any) as ActionPayload;
switch (actionResolutionType) {
case props.identifier:
actionPayloads = [];
actionPayloads.push(({
...actionPayload,
actionId: undefined,
onSuccess: undefined,
onError: undefined,
} as any) as ActionPayload);
break;
case ACTION_RESOLUTION_TYPE.SUCCESS:
const successActionPayload =
actionPayload.onSuccess !== undefined
? actionPayload.onSuccess[0]
: undefined;
const successActionPayloads = [];
successActionPayloads.push({
...successActionPayload,
actionId: "",
});
actionPayload.onSuccess = successActionPayloads as ActionPayload[];
break;
case ACTION_RESOLUTION_TYPE.ERROR:
const errorActionPayload =
actionPayload.onError !== undefined
? actionPayload.onError[0]
: undefined;
const errorActionPayloads = [];
errorActionPayloads.push({
...errorActionPayload,
actionId: "",
});
actionPayload.onError = errorActionPayloads as ActionPayload[];
break;
}
props.updateActions(actionPayloads, props.identifier);
}
const onActionTypeSelect = (item: DropdownOption) => {
const actionPayloads: ActionPayload[] = props.actions
? props.actions.slice()
: [];
const actionPayload = actionPayloads[0];
if (actionPayload && actionPayload.actionType !== item.value) {
actionPayload.actionId = "";
actionPayload.onError = undefined;
actionPayload.onSuccess = undefined;
actionPayload.actionType = item.value as ActionType;
} else {
const actionPayload = { actionType: item.value } as ActionPayload;
actionPayloads.push(actionPayload);
}
props.updateActions(actionPayloads, props.identifier);
};
const onSuccessActionTypeSelect = (item: DropdownOption) => {
const actionPayloads: ActionPayload[] = props.actions
? props.actions.slice()
: [];
const actionPayload = actionPayloads[0];
if (actionPayload) {
const successActionPayloads: ActionPayload[] =
actionPayload.onSuccess || [];
const successActionPayload = successActionPayloads[0];
if (successActionPayload) {
successActionPayload.actionId = "";
successActionPayload.actionType = item.value as ActionType;
} else {
const successActionPayload = {
actionType: item.value,
} as ActionPayload;
successActionPayloads.push(successActionPayload);
}
actionPayload.onSuccess = successActionPayloads;
}
props.updateActions(actionPayloads, props.identifier);
};
const onErrorActionTypeSelect = (item: DropdownOption) => {
const actionPayloads: ActionPayload[] = props.actions
? props.actions.slice()
: [];
const actionPayload = actionPayloads[0];
if (actionPayload) {
const errorActionPayloads: ActionPayload[] = actionPayload.onError || [];
const errorActionPayload = errorActionPayloads[0];
if (errorActionPayload) {
errorActionPayload.actionId = "";
errorActionPayload.actionType = item.value as ActionType;
} else {
const errorActionPayload = {
actionType: item.value,
} as ActionPayload;
errorActionPayloads.push(errorActionPayload);
}
actionPayload.onError = errorActionPayloads;
}
props.updateActions(actionPayloads, props.identifier);
};
const onActionSelect = (item: DropdownOption): void => {
const actionPayloads: ActionPayload[] = props.actions
? props.actions.slice()
: [];
const actionPayload = actionPayloads[0];
actionPayload.actionId = item.value;
props.updateActions(actionPayloads, props.identifier);
};
const onSuccessActionSelect = (item: DropdownOption): void => {
const actionPayloads: ActionPayload[] = props.actions
? props.actions.slice()
: [];
const actionPayload = actionPayloads[0];
const successActionPayloads: ActionPayload[] = actionPayload.onSuccess as ActionPayload[];
const successActionPayload = successActionPayloads[0];
successActionPayload.actionId = item.value;
actionPayload.onSuccess = successActionPayloads;
props.updateActions(actionPayloads, props.identifier);
};
const onErrorActionSelect = (item: DropdownOption): void => {
const actionPayloads: ActionPayload[] = props.actions
? props.actions.slice()
: [];
const actionPayload = actionPayloads[0];
const errorActionPayloads: ActionPayload[] = actionPayload.onError as ActionPayload[];
const errorActionPayload = errorActionPayloads[0];
errorActionPayload.actionId = item.value;
actionPayload.onError = errorActionPayloads;
props.updateActions(actionPayloads, props.identifier);
};
let onTypeSelect = onActionTypeSelect;
switch (props.actionResolutionType) {
case ACTION_RESOLUTION_TYPE.SUCCESS:
onTypeSelect = onSuccessActionTypeSelect;
break;
case ACTION_RESOLUTION_TYPE.ERROR:
onTypeSelect = onErrorActionTypeSelect;
break;
}
let onActionSelectHandler = onActionSelect;
switch (props.actionResolutionType) {
case ACTION_RESOLUTION_TYPE.SUCCESS:
onActionSelectHandler = onSuccessActionSelect;
break;
case ACTION_RESOLUTION_TYPE.ERROR:
onActionSelectHandler = onErrorActionSelect;
break;
}
const showActionTypeRemoveButton =
props.selectedActionType &&
props.selectedActionType !== DEFAULT_ACTION_TYPE;
const showActionLabelRemoveButton =
props.selectedActionLabel &&
props.selectedActionLabel !== DEFAULT_ACTION_LABEL;
const onTextChange = (
event: React.ChangeEvent<HTMLTextAreaElement> | string,
) => {
let value = event;
if (typeof event !== "string") {
value = event.target.value;
}
!!props.updateLabel &&
props.updateLabel((value as any) as string, props.identifier);
// props.updateProperty(this.props.propertyName, value);
};
return (
<div>
<div hidden={props.labelEditable}>
<label>{props.identifier}</label>
</div>
<div hidden={!props.labelEditable}>
<InputText
label={"Action Name"}
value={props.label}
onChange={onTextChange}
isValid={true}
></InputText>
<label>{"On Action Click"}</label>
</div>
<ActionSelectorDropDownContainer>
<ActionSelectorDropDown
items={props.actionTypeOptions}
filterable={false}
itemRenderer={renderItem}
onItemSelect={onTypeSelect}
noResults={<MenuItem disabled={true} text="No results." />}
>
{
<Button
text={props.selectedActionType}
rightIcon={showActionTypeRemoveButton ? false : "chevron-down"}
/>
}
</ActionSelectorDropDown>
{showActionTypeRemoveButton && (
<CloseButton
size={theme.spaces[5]}
color={theme.colors.paneSectionLabel}
onClick={() => {
clearActionSelectorType(props.actionResolutionType);
}}
></CloseButton>
)}
</ActionSelectorDropDownContainer>
<ActionSelectorDropDownContainer>
{props.selectedActionType !== DEFAULT_ACTION_TYPE && (
<ActionSelectorDropDown
items={props.allActions}
filterable={false}
itemRenderer={renderItem}
onItemSelect={onActionSelectHandler}
noResults={<MenuItem disabled={true} text="No results." />}
>
<Button
text={props.selectedActionLabel}
rightIcon={showActionLabelRemoveButton ? false : "chevron-down"}
/>
</ActionSelectorDropDown>
)}
{showActionLabelRemoveButton && (
<CloseButton
size={theme.spaces[5]}
color={theme.colors.paneSectionLabel}
onClick={() => {
clearActionSelectorLabel(props.actionResolutionType);
}}
></CloseButton>
)}
</ActionSelectorDropDownContainer>
</div>
);
}

View File

@ -2,221 +2,30 @@ import React from "react";
import BaseControl, { ControlProps } from "./BaseControl";
import { ControlWrapper } from "./StyledControls";
import { ControlType } from "constants/PropertyControlConstants";
import {
ActionPayload,
ActionType,
PropertyPaneActionDropdownOptions,
} from "constants/ActionConstants";
import { DropdownOption } from "widgets/DropdownWidget";
import { connect } from "react-redux";
import { AppState } from "reducers";
import styled from "styled-components";
import ActionSelector from "./ActionSelector";
import { RestAction } from "api/ActionAPI";
import DynamicActionCreator from "components/editorComponents/DynamicActionCreator";
const DEFAULT_ACTION_TYPE = "Select Action Type" as ActionType;
const DEFAULT_ACTION_LABEL = "Select Action";
enum ACTION_RESOLUTION_TYPE {
SUCCESS,
ERROR,
}
const ResolutionActionContainer = styled.div`
padding: 0px 0px;
`;
function getActions(
actionPayloads: ActionPayload[] | undefined,
): {
action: ActionPayload | undefined;
onSuccessAction: ActionPayload | undefined;
onErrorAction: ActionPayload | undefined;
} {
const action: ActionPayload | undefined = actionPayloads && actionPayloads[0];
const onSuccessAction: ActionPayload | undefined =
action && action.onSuccess && action.onSuccess[0];
const onErrorAction: ActionPayload | undefined =
action && action.onError && action.onError[0];
return {
action,
onSuccessAction,
onErrorAction,
class ActionSelectorControl extends BaseControl<ControlProps> {
handleValueUpdate = (newValue: string) => {
const { propertyName } = this.props;
this.updateProperty(propertyName, newValue);
};
}
interface FinalActionSelectorProps {
actions: ActionPayload[];
identifier: string;
actionsData: RestAction[];
label: string;
labelEditable?: boolean;
updateLabel?: (label: string, key: string) => void;
updateActions: (actions: ActionPayload[], key?: string) => void;
}
export function FinalActionSelector(props: FinalActionSelectorProps) {
function getSelectionActionType(
type: ACTION_RESOLUTION_TYPE | string,
): ActionType {
let selectedActionTypeValue: ActionType | undefined;
const { action, onSuccessAction, onErrorAction } = getActions(
props.actions,
);
switch (type) {
case props.identifier:
selectedActionTypeValue = action && action.actionType;
break;
case ACTION_RESOLUTION_TYPE.SUCCESS:
selectedActionTypeValue = onSuccessAction && onSuccessAction.actionType;
break;
case ACTION_RESOLUTION_TYPE.ERROR:
selectedActionTypeValue = onErrorAction && onErrorAction.actionType;
break;
default:
break;
}
const foundActionType = PropertyPaneActionDropdownOptions.find(
actionType => actionType.value === selectedActionTypeValue,
);
return foundActionType
? (foundActionType.label as ActionType)
: DEFAULT_ACTION_TYPE;
}
function getSelectionActionLabel(
type: ACTION_RESOLUTION_TYPE | string,
allActions: DropdownOption[],
): string {
let selectedActionId: string | undefined = "";
const { action, onSuccessAction, onErrorAction } = getActions(
props.actions,
);
switch (type) {
case props.identifier:
selectedActionId = action && action.actionId;
break;
case ACTION_RESOLUTION_TYPE.SUCCESS:
selectedActionId = onSuccessAction && onSuccessAction.actionId;
break;
case ACTION_RESOLUTION_TYPE.ERROR:
selectedActionId = onErrorAction && onErrorAction.actionId;
break;
default:
break;
}
const foundAction = allActions.find(
action => action.value === selectedActionId,
);
return foundAction ? foundAction.label : DEFAULT_ACTION_LABEL;
}
const actionTypeOptions: DropdownOption[] = PropertyPaneActionDropdownOptions;
const allActions = props.actionsData.map(action => {
return {
label: action.name,
value: action.id,
id: action.id,
};
});
const selectedActionType = getSelectionActionType(props.identifier);
const selectedActionLabel = getSelectionActionLabel(
props.identifier,
allActions,
);
const selectedSuccessActionType = getSelectionActionType(
ACTION_RESOLUTION_TYPE.SUCCESS,
);
const selectedSuccessActionLabel = getSelectionActionLabel(
ACTION_RESOLUTION_TYPE.SUCCESS,
allActions,
);
const selectedErrorActionType = getSelectionActionType(
ACTION_RESOLUTION_TYPE.ERROR,
);
const selectedErrorActionLabel = getSelectionActionLabel(
ACTION_RESOLUTION_TYPE.ERROR,
allActions,
);
return (
<ControlWrapper>
<ActionSelector
allActions={allActions}
actionTypeOptions={actionTypeOptions}
selectedActionType={selectedActionType}
selectedActionLabel={selectedActionLabel}
label={props.label}
identifier={props.identifier}
actionResolutionType={props.identifier}
updateActions={props.updateActions}
updateLabel={props.updateLabel}
actions={props.actions}
labelEditable={props.labelEditable}
/>
{selectedActionLabel !== DEFAULT_ACTION_LABEL && (
<ResolutionActionContainer>
<ActionSelector
allActions={allActions}
actionTypeOptions={actionTypeOptions}
selectedActionType={selectedSuccessActionType}
selectedActionLabel={selectedSuccessActionLabel}
identifier={"On Success"}
label={"On Success"}
actionResolutionType={ACTION_RESOLUTION_TYPE.SUCCESS}
updateActions={props.updateActions}
updateLabel={props.updateLabel}
actions={props.actions}
/>
<ActionSelector
allActions={allActions}
actionTypeOptions={actionTypeOptions}
selectedActionType={selectedErrorActionType}
selectedActionLabel={selectedErrorActionLabel}
identifier={"On Error"}
label={"On Error"}
actionResolutionType={ACTION_RESOLUTION_TYPE.ERROR}
updateActions={props.updateActions}
updateLabel={props.updateLabel}
actions={props.actions}
/>
</ResolutionActionContainer>
)}
</ControlWrapper>
);
}
class ActionSelectorControl extends BaseControl<
ControlProps & { data: RestAction[] }
> {
render() {
const { propertyValue } = this.props;
return (
<FinalActionSelector
actionsData={this.props.data}
label={this.props.propertyName}
actions={this.props.propertyValue}
identifier={this.props.propertyName}
updateActions={this.updateActions}
/>
<ControlWrapper>
<label>{this.props.label}</label>
<DynamicActionCreator
value={propertyValue}
onValueChange={this.handleValueUpdate}
/>
</ControlWrapper>
);
}
updateActions = (actions: ActionPayload[]) => {
this.updateProperty(this.props.propertyName, actions);
};
getControlType(): ControlType {
return "ACTION_SELECTOR";
}
}
export interface ActionSelectorControlProps extends ControlProps {
propertyValue: ActionPayload[];
}
const mapStateToProps = (state: AppState): { data: RestAction[] } => ({
data: state.entities.actions.map(a => a.config),
});
export default connect(mapStateToProps)(ActionSelectorControl);
export default ActionSelectorControl;

View File

@ -6,7 +6,10 @@ import { Component } from "react";
import _ from "lodash";
import { ControlType } from "constants/PropertyControlConstants";
abstract class BaseControl<T extends ControlProps> extends Component<T> {
abstract class BaseControl<P extends ControlProps, S = {}> extends Component<
P,
S
> {
updateProperty(propertyName: string, propertyValue: any) {
if (!_.isNil(this.props.onPropertyChange))
this.props.onPropertyChange(propertyName, propertyValue);

View File

@ -3,21 +3,18 @@ import React from "react";
import BaseControl, { ControlProps } from "./BaseControl";
import { ControlWrapper, StyledPropertyPaneButton } from "./StyledControls";
import { ControlType } from "constants/PropertyControlConstants";
import { AppState } from "reducers";
import { connect } from "react-redux";
import { ActionPayload } from "constants/ActionConstants";
import { FinalActionSelector } from "./ActionSelectorControl";
import { generateReactKey } from "utils/generators";
import styled from "constants/DefaultTheme";
import { AnyStyledComponent } from "styled-components";
import { FormIcons } from "icons/FormIcons";
import { RestAction } from "api/ActionAPI";
import { InputText } from "components/propertyControls/InputTextControl";
import DynamicActionCreator from "components/editorComponents/DynamicActionCreator";
export interface ColumnAction {
label: string;
id: string;
actionPayloads: ActionPayload[];
dynamicTrigger: string;
}
const StyledDeleteIcon = styled(FormIcons.DELETE_ICON as AnyStyledComponent)`
padding: 5px 5px;
position: absolute;
@ -27,40 +24,44 @@ const StyledDeleteIcon = styled(FormIcons.DELETE_ICON as AnyStyledComponent)`
`;
class ColumnActionSelectorControl extends BaseControl<
ColumnActionSelectorControlProps & { data: RestAction[] }
ColumnActionSelectorControlProps
> {
render() {
return (
<ControlWrapper orientation={"VERTICAL"}>
{this.props.propertyValue &&
this.props.propertyValue.map(
(columnAction: ColumnAction, index: number) => {
return (
<div
key={columnAction.id}
style={{
position: "relative",
// position: "absolute",
}}
>
<FinalActionSelector
identifier={columnAction.id}
actionsData={this.props.data}
actions={columnAction.actionPayloads}
updateActions={this.updateActions}
label={columnAction.label}
labelEditable={true}
updateLabel={this.updateLabel}
/>
<StyledDeleteIcon
height={20}
width={20}
onClick={this.removeColumnAction.bind(this, index)}
/>
</div>
);
},
)}
this.props.propertyValue.map((columnAction: ColumnAction) => {
return (
<div
key={columnAction.id}
style={{
position: "relative",
}}
>
<InputText
label={columnAction.label}
value={columnAction.label}
onChange={this.updateColumnActionLabel.bind(
this,
columnAction,
)}
isValid={true}
/>
<DynamicActionCreator
value={columnAction.dynamicTrigger}
onValueChange={this.updateColumnActionFunction.bind(
this,
columnAction,
)}
/>
<StyledDeleteIcon
height={20}
width={20}
onClick={this.removeColumnAction.bind(this, columnAction)}
/>
</div>
);
})}
<StyledPropertyPaneButton
text={"Column Action"}
icon={"plus"}
@ -71,67 +72,50 @@ class ColumnActionSelectorControl extends BaseControl<
</ControlWrapper>
);
}
onTextChange = (event: React.ChangeEvent<HTMLTextAreaElement> | string) => {
let value = event;
if (typeof event !== "string") {
value = event.target.value;
updateColumnActionLabel = (
columnAction: ColumnAction,
newValue: React.ChangeEvent<HTMLTextAreaElement> | string,
) => {
let value = newValue;
if (typeof newValue !== "string") {
value = newValue.target.value;
}
this.updateProperty(this.props.propertyName, value);
const update = this.props.propertyValue.map((a: ColumnAction) => {
if (a.id === columnAction.id) return { ...a, label: value };
return a;
});
this.updateProperty(this.props.propertyName, update);
};
updateActions = (actions: ActionPayload[], key?: string) => {
const columnActions = this.props.propertyValue || [];
const columnActionsClone = columnActions.slice();
const foundColumnActionIndex = columnActionsClone.findIndex(
(columnAction: ColumnAction) => columnAction.id === key,
updateColumnActionFunction = (
columnAction: ColumnAction,
newValue: string,
) => {
const update = this.props.propertyValue.map((a: ColumnAction) => {
if (a.id === columnAction.id) return { ...a, dynamicTrigger: newValue };
return a;
});
this.updateProperty(this.props.propertyName, update);
};
removeColumnAction = (columnAction: ColumnAction) => {
const update = this.props.propertyValue.filter(
(a: ColumnAction) => a.id !== columnAction.id,
);
if (foundColumnActionIndex !== -1) {
let foundColumnAction = columnActionsClone[foundColumnActionIndex];
foundColumnAction = Object.assign({}, foundColumnAction);
foundColumnAction.actionPayloads = actions;
columnActionsClone.splice(foundColumnActionIndex, 1, foundColumnAction);
}
this.updateProperty(this.props.propertyName, columnActionsClone);
};
updateLabel = (label: string, key: string) => {
const columnActions = this.props.propertyValue || [];
const columnActionsClone = columnActions.slice();
const foundColumnActionIndex = columnActionsClone.findIndex(
(columnAction: ColumnAction) => columnAction.id === key,
);
if (foundColumnActionIndex !== -1) {
let foundColumnAction = columnActionsClone[foundColumnActionIndex];
foundColumnAction = Object.assign({}, foundColumnAction);
foundColumnAction.label = label;
columnActionsClone.splice(foundColumnActionIndex, 1, foundColumnAction);
}
this.updateProperty(this.props.propertyName, columnActionsClone);
};
removeColumnAction = (index: number) => {
const columnActions = this.props.propertyValue || [];
const columnActionsClone = columnActions.slice();
columnActionsClone.splice(index, 1);
this.updateProperty(this.props.propertyName, columnActionsClone);
this.updateProperty(this.props.propertyName, update);
};
addColumnAction = () => {
const columnActions = this.props.propertyValue || [];
const columnActionsClone = columnActions.slice();
columnActionsClone.push({
label: "Action",
id: generateReactKey(),
actionPayloads: [],
});
const update = columnActions.concat([
{
label: "Action",
id: generateReactKey(),
actionPayloads: [],
},
]);
this.updateProperty(this.props.propertyName, columnActionsClone);
};
onToggle = () => {
this.updateProperty(this.props.propertyName, !this.props.propertyValue);
this.updateProperty(this.props.propertyName, update);
};
getControlType(): ControlType {
@ -141,8 +125,4 @@ class ColumnActionSelectorControl extends BaseControl<
export type ColumnActionSelectorControlProps = ControlProps;
const mapStateToProps = (state: AppState): { data: RestAction[] } => ({
data: state.entities.actions.map(a => a.config),
});
export default connect(mapStateToProps)(ColumnActionSelectorControl);
export default ColumnActionSelectorControl;

View File

@ -2,7 +2,11 @@ import React from "react";
import BaseControl, { ControlProps } from "./BaseControl";
import { Button, MenuItem } from "@blueprintjs/core";
import { IItemRendererProps } from "@blueprintjs/select";
import { ControlWrapper, StyledDropDown } from "./StyledControls";
import {
ControlWrapper,
StyledDropDown,
StyledDropDownContainer,
} from "./StyledControls";
import { DropdownOption } from "widgets/DropdownWidget";
import { ControlType } from "constants/PropertyControlConstants";
@ -14,18 +18,24 @@ class DropDownControl extends BaseControl<DropDownControlProps> {
return (
<ControlWrapper>
<label>{this.props.label}</label>
<StyledDropDown
items={this.props.options}
filterable={false}
itemRenderer={this.renderItem}
onItemSelect={this.onItemSelect}
noResults={<MenuItem disabled={true} text="No results." />}
>
<Button
text={selected ? selected.label : ""}
rightIcon="chevron-down"
/>
</StyledDropDown>
<StyledDropDownContainer>
<StyledDropDown
items={this.props.options}
filterable={false}
itemRenderer={this.renderItem}
onItemSelect={this.onItemSelect}
noResults={<MenuItem disabled={true} text="No results." />}
popoverProps={{
minimal: true,
usePortal: false,
}}
>
<Button
text={selected ? selected.label : ""}
rightIcon="chevron-down"
/>
</StyledDropDown>
</StyledDropDownContainer>
</ControlWrapper>
);
}
@ -41,8 +51,8 @@ class DropDownControl extends BaseControl<DropDownControlProps> {
const isSelected: boolean = this.isOptionSelected(option);
return (
<MenuItem
icon={isSelected ? "tick" : "blank"}
active={itemProps.modifiers.active}
className="single-select"
active={isSelected}
key={option.value}
onClick={itemProps.handleClick}
text={option.label}

View File

@ -1,10 +1,11 @@
import styled from "styled-components";
import { Select, MultiSelect } from "@blueprintjs/select";
import { Switch, InputGroup, Button } from "@blueprintjs/core";
import { Switch, InputGroup, Button, Classes } from "@blueprintjs/core";
import { DropdownOption } from "widgets/DropdownWidget";
import { ContainerOrientation } from "constants/WidgetConstants";
import { DateInput } from "@blueprintjs/datetime";
import { TimezonePicker } from "@blueprintjs/timezone";
import { Colors } from "constants/Colors";
type ControlWrapperProps = {
orientation?: ContainerOrientation;
@ -29,12 +30,90 @@ export const ControlWrapper = styled.div<ControlWrapperProps>`
}
`;
export const StyledDropDownContainer = styled.div`
&&&& .${Classes.BUTTON} {
box-shadow: none;
border-radius: 4px;
background-color: ${Colors.SHARK};
color: ${Colors.CADET_BLUE};
background-image: none;
}
&&&& .${Classes.MENU_ITEM} {
border-radius: ${props => props.theme.radii[1]}px;
&:hover {
background: ${Colors.POLAR};
}
&.${Classes.ACTIVE} {
background: ${Colors.POLAR};
color: ${props => props.theme.colors.textDefault};
position: relative;
&.single-select {
&:before {
left: 0;
top: -2px;
position: absolute;
content: "";
background: ${props => props.theme.colors.primary};
border-radius: 4px 0 0 4px;
width: 4px;
height: 100%;
}
}
}
}
&& .${Classes.POPOVER} {
width: 100%;
border-radius: ${props => props.theme.radii[1]}px;
box-shadow: 0px 2px 4px rgba(67, 70, 74, 0.14);
padding: ${props => props.theme.spaces[3]}px;
background: white;
}
&&&& .${Classes.POPOVER_CONTENT} {
box-shadow: none;
}
&& .${Classes.POPOVER_WRAPPER} {
.${Classes.OVERLAY} {
.${Classes.TRANSITION_CONTAINER} {
width: 100%;
}
}
}
&& .${Classes.MENU} {
max-width: 100%;
max-height: auto;
}
width: 100%;
`;
const DropDown = Select.ofType<DropdownOption>();
export const StyledDropDown = styled(DropDown)`
&&& button {
background: ${props => props.theme.colors.paneInputBG};
color: ${props => props.theme.colors.textOnDarkBG};
box-shadow: none;
div {
flex: 1 1 auto;
}
span {
width: 100%;
position: relative;
}
.${Classes.BUTTON} {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
}
.${Classes.BUTTON_TEXT} {
text-overflow: ellipsis;
text-align: left;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
&& {
.${Classes.ICON} {
width: fit-content;
color: ${Colors.SLATE_GRAY};
}
}
`;

View File

@ -1,17 +1,30 @@
import { AlertType, MessageIntent } from "widgets/AlertWidget";
import { DropdownOption } from "widgets/DropdownWidget";
export type ExecuteActionPayload = {
dynamicString: string;
event: {
type: EventType;
};
responseData?: any;
};
export type EventType =
| "ON_CLICK"
| "ON_HOVER"
| "ON_TOGGLE"
| "ON_LOAD"
| "ON_TEXT_CHANGE"
| "ON_SUBMIT"
| "ON_CHECK_CHANGE"
| "ON_SELECT"
| "ON_DATE_SELECTED"
| "ON_DATE_RANGE_SELECTED";
export enum EventType {
ON_PAGE_LOAD = "ON_PAGE_LOAD",
ON_PREV_PAGE = "ON_PREV_PAGE",
ON_NEXT_PAGE = "ON_NEXT_PAGE",
ON_ERROR = "ON_ERROR",
ON_SUCCESS = "ON_SUCCESS",
ON_ROW_SELECTED = "ON_ROW_SELECTED",
ON_CLICK = "ON_CLICK",
ON_HOVER = "ON_HOVER",
ON_TOGGLE = "ON_TOGGLE",
ON_LOAD = "ON_LOAD",
ON_TEXT_CHANGE = "ON_TEXT_CHANGE",
ON_SUBMIT = "ON_SUBMIT",
ON_CHECK_CHANGE = "ON_CHECK_CHANGE",
ON_SELECT = "ON_SELECT",
ON_DATE_SELECTED = "ON_DATE_SELECTED",
ON_DATE_RANGE_SELECTED = "ON_DATE_RANGE_SELECTED",
ON_OPTION_CHANGE = "ON_OPTION_CHANGE",
}
export type ActionType =
| "API"
@ -22,60 +35,8 @@ export type ActionType =
| "SET_VALUE"
| "DOWNLOAD";
export const PropertyPaneActionDropdownOptions: DropdownOption[] = [
{ label: "Call API", value: "API" },
// { label: "Run Query", value: "QUERY" },
];
export interface BaseActionPayload {
actionId: string;
actionType: ActionType;
contextParams: Record<string, string>;
onSuccess?: ActionPayload[];
onError?: ActionPayload[];
}
export type ActionPayload =
| NavigateActionPayload
| SetValueActionPayload
| ExecuteJSActionPayload
| DownloadDataActionPayload
| TableAction;
export type NavigationType = "NEW_TAB" | "INLINE";
export interface NavigateActionPayload extends BaseActionPayload {
pageUrl: string;
navigationType: NavigationType;
}
export interface ShowAlertActionPayload extends BaseActionPayload {
header: string;
message: string;
alertType: AlertType;
intent: MessageIntent;
}
export interface SetValueActionPayload extends BaseActionPayload {
header: string;
message: string;
alertType: AlertType;
intent: MessageIntent;
}
export interface ExecuteJSActionPayload extends BaseActionPayload {
jsFunctionId: string;
jsFunction: string;
}
export type DownloadFiletype = "CSV" | "XLS" | "JSON" | "TXT";
export interface DownloadDataActionPayload extends BaseActionPayload {
data: JSON;
fileName: string;
fileType: DownloadFiletype;
}
export interface PageAction {
id: string;
pluginType: ActionType;
@ -83,10 +44,6 @@ export interface PageAction {
jsonPathKeys: string[];
}
export interface TableAction extends BaseActionPayload {
actionName: string;
}
export interface ExecuteErrorPayload {
actionId: string;
error: any;

View File

@ -35,6 +35,9 @@ export const Colors: Record<string, string> = {
JAFFA: "#F2994A",
BLUE_BAYOUX: "#4E5D78",
MINT_TULIP: "#B5F1F1",
AZURE_RADIANCE: "#0384FE",
OCEAN_GREEN: "#36AB80",
BUTTER_CUP: "#F7AF22",
};
export type Color = typeof Colors[keyof typeof Colors];

View File

@ -260,6 +260,7 @@ export type Theme = {
};
};
pageContentWidth: number;
alert: Record<string, { color: Color }>;
};
export const getColorWithOpacity = (color: Color, opacity: number) => {
@ -398,6 +399,20 @@ export const theme: Theme = {
},
},
pageContentWidth: 1224,
alert: {
info: {
color: Colors.AZURE_RADIANCE,
},
success: {
color: Colors.OCEAN_GREEN,
},
error: {
color: Colors.RED,
},
warning: {
color: Colors.BUTTER_CUP,
},
},
};
export { css, createGlobalStyle, keyframes, ThemeProvider };

View File

@ -140,6 +140,8 @@ export const ReduxActionTypes: { [key: string]: string } = {
ADD_USER_TO_ORG_INIT: "ADD_USER_TO_ORG_INIT",
ADD_USER_TO_ORG_SUCCESS: "ADD_USER_TO_ORG_ERROR",
SET_META_PROP: "SET_META_PROP",
EXECUTE_API_ACTION_REQUEST: "EXECUTE_API_ACTION_REQUEST",
EXECUTE_API_ACTION_SUCCESS: "EXECUTE_API_ACTION_SUCCESS",
};
export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes];
@ -200,6 +202,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
SET_DEFAULT_APPLICATION_PAGE_ERROR: "SET_DEFAULT_APPLICATION_PAGE_ERROR",
CREATE_ORGANIZATION_ERROR: "CREATE_ORGANIZATION_ERROR",
ADD_USER_TO_ORG_ERROR: "ADD_USER_TO_ORG_ERROR",
EXECUTE_API_ACTION_ERROR: "EXECUTE_API_ACTION_ERROR",
};
export const ReduxFormActionTypes: { [key: string]: string } = {

View File

@ -1,2 +0,0 @@
export const ENTITY_TYPE_ACTION = "ACTION";
export const ENTITY_TYPE_WIDGET = "WIDGET";

View File

@ -0,0 +1,94 @@
import { ActionData } from "reducers/entityReducers/actionsReducer";
import { WidgetProps } from "widgets/BaseWidget";
import { AppState } from "reducers";
import { ActionResponse } from "api/ActionAPI";
export type ActionDescription<T> = {
type: string;
payload: T;
};
type ActionDispatcher<T, A extends string[]> = (
...args: A
) => ActionDescription<T>;
export enum ENTITY_TYPE {
ACTION = "ACTION",
WIDGET = "WIDGET",
}
export type RunActionPayload = {
actionId: string;
onSuccess: string;
onError: string;
};
export interface DataTreeAction extends Omit<ActionData, "data"> {
data: ActionResponse["body"];
run: ActionDispatcher<RunActionPayload, [string, string]>;
ENTITY_TYPE: ENTITY_TYPE.ACTION;
}
export interface DataTreeWidget extends WidgetProps {
ENTITY_TYPE: ENTITY_TYPE.WIDGET;
}
export type DataTree = {
[key: string]: DataTreeAction | DataTreeWidget | ActionDispatcher<any, any>;
} & { actionPaths?: string[] };
export class DataTreeFactory {
static create(state: AppState): DataTree {
const dataTree: DataTree = {};
dataTree.actionPaths = ["navigateTo", "navigateToUrl", "showAlert"];
state.entities.actions.forEach(a => {
dataTree[a.config.name] = {
...a,
data: a.data ? a.data.body : {},
run: function(onSuccess: string, onError: string) {
return {
type: "RUN_ACTION",
payload: {
actionId: this.config.id,
onSuccess: onSuccess ? `{{${onSuccess.toString()}}}` : "",
onError: onError ? `{{${onError.toString()}}}` : "",
},
};
},
ENTITY_TYPE: ENTITY_TYPE.ACTION,
};
dataTree.actionPaths && dataTree.actionPaths.push(`${a.config.name}.run`);
});
Object.keys(state.entities.canvasWidgets).forEach(w => {
const widget = state.entities.canvasWidgets[w];
const widgetMetaProps = state.entities.meta[w];
dataTree[widget.widgetName] = {
...widget,
...widgetMetaProps,
ENTITY_TYPE: ENTITY_TYPE.WIDGET,
};
});
dataTree.navigateTo = function(pageName: string) {
return {
type: "NAVIGATE_TO",
payload: { pageName },
};
};
dataTree.navigateToUrl = function(url: string) {
return {
type: "NAVIGATE_TO_URL",
payload: { url },
};
};
dataTree.showAlert = function(message: string, style: string) {
return {
type: "SHOW_ALERT",
payload: { message, style },
};
};
return dataTree;
}
}

View File

@ -0,0 +1,35 @@
import React from "react";
import { IconProps, IconWrapper } from "constants/IconConstants";
import { ReactComponent as InfoIcon } from "assets/icons/alert/info.svg";
import { ReactComponent as SuccessIcon } from "assets/icons/alert/success.svg";
import { ReactComponent as ErrorIcon } from "assets/icons/alert/error.svg";
import { ReactComponent as WarningIcon } from "assets/icons/alert/warning.svg";
/* eslint-disable react/display-name */
export const AlertIcons: {
[id: string]: Function;
} = {
INFO: (props: IconProps) => (
<IconWrapper {...props}>
<InfoIcon />
</IconWrapper>
),
SUCCESS: (props: IconProps) => (
<IconWrapper {...props}>
<SuccessIcon />
</IconWrapper>
),
ERROR: (props: IconProps) => (
<IconWrapper {...props}>
<ErrorIcon />
</IconWrapper>
),
WARNING: (props: IconProps) => (
<IconWrapper {...props}>
<WarningIcon />
</IconWrapper>
),
};
export type AlertIconName = keyof typeof AlertIcons;

View File

@ -121,4 +121,12 @@ div.bp3-popover-arrow {
.display-none {
display: none;
}
}
.Toastify__toast {
padding: 0 !important;
border-radius: 4px !important;
}
.Toastify__toast-body {
margin: 0 !important;
}

View File

@ -13,6 +13,7 @@ import TouchBackend from "react-dnd-touch-backend";
import { appInitializer } from "utils/AppsmithUtils";
import ProtectedRoute from "./pages/common/ProtectedRoute";
import { Slide, ToastContainer } from "react-toastify";
import store from "./store";
import {
BASE_URL,
@ -49,6 +50,13 @@ ReactDOM.render(
>
<Provider store={store}>
<ThemeProvider theme={theme}>
<ToastContainer
hideProgressBar
draggable={false}
transition={Slide}
autoClose={5000}
closeButton={false}
/>
<Helmet>
<meta charSet="utf-8" />
<link rel="shortcut icon" href="/favicon-orange.ico" />

View File

@ -1,9 +1,18 @@
import RealmExecutor from "./RealmExecutor";
import moment from "moment-timezone";
import { ActionDescription } from "entities/DataTree/dataTreeFactory";
export type JSExecutorGlobal = Record<string, object>;
export type JSExecutorResult = {
result: any;
triggers?: ActionDescription<any>[];
};
export interface JSExecutor {
execute: (src: string, data: JSExecutorGlobal) => any;
execute: (
src: string,
data: JSExecutorGlobal,
callbackData?: any,
) => JSExecutorResult;
registerLibrary: (accessor: string, lib: any) => void;
unRegisterLibrary: (accessor: string) => void;
}
@ -56,8 +65,12 @@ class JSExecutionManager {
this.registerLibrary(config.accessor, config.lib);
});
}
evaluateSync(jsSrc: string, data: JSExecutorGlobal) {
return this.currentExecutor.execute(jsSrc, data);
evaluateSync(
jsSrc: string,
data: JSExecutorGlobal,
callbackData?: any,
): JSExecutorResult {
return this.currentExecutor.execute(jsSrc, data, callbackData);
}
}
const JSExecutionManagerSingleton = new JSExecutionManager();

View File

@ -1,4 +1,9 @@
import { JSExecutorGlobal, JSExecutor } from "./JSExecutionManagerSingleton";
import {
JSExecutorGlobal,
JSExecutor,
JSExecutorResult,
} from "./JSExecutionManagerSingleton";
import JSONFn from "json-fn";
declare let Realm: any;
export default class RealmExecutor implements JSExecutor {
@ -10,19 +15,40 @@ export default class RealmExecutor implements JSExecutor {
libraries: Record<string, any> = {};
constructor() {
this.rootRealm = Realm.makeRootRealm();
this.registerLibrary("JSONFn", JSONFn);
this.createSafeFunction = this.rootRealm.evaluate(`
(function createSafeFunction(unsafeFn) {
return function safeFn(...args) {
unsafeFn(...args);
return unsafeFn(...args);
}
})
`);
this.createSafeObject = this.rootRealm.evaluate(`
(function creaetSafeObject(unsafeObject) {
return JSON.parse(JSON.stringify(unsafeObject));
// After parsing the data we add a triggers list on the global scope to
// push to it during any script execution
// We replace all action descriptor functions with our pusher function
// which has reference to the triggers via binding
this.createSafeObject = this.rootRealm.evaluate(
`
(function createSafeObject(unsafeObject) {
const safeObject = JSONFn.parse(JSONFn.stringify(unsafeObject));
if(safeObject.actionPaths) {
safeObject.triggers = [];
const pusher = function (action, ...payload) {
const actionPayload = action(...payload);
this.triggers.push(actionPayload);
}
safeObject.actionPaths.forEach(path => {
const action = _.get(safeObject, path);
const entity = _.get(safeObject, path.split(".")[0])
_.set(safeObject, path, pusher.bind(safeObject, action.bind(entity)))
})
}
return safeObject
})
`);
`,
);
}
registerLibrary(accessor: string, lib: any) {
this.rootRealm.global[accessor] = lib;
}
@ -38,14 +64,46 @@ export default class RealmExecutor implements JSExecutor {
}
return result;
}
execute(sourceText: string, data: JSExecutorGlobal) {
execute(
sourceText: string,
data: JSExecutorGlobal,
callbackData?: any,
): JSExecutorResult {
const safeCallbackData = this.createSafeObject(callbackData || {});
const safeData = this.createSafeObject(data);
let result;
try {
result = this.rootRealm.evaluate(sourceText, safeData);
// We create a closed function and evaluate that
// This is to send any triggers received during evaluations
// triggers should already be defined in the safeData
const scriptToEvaluate = `
function closedFunction () {
const result = ${sourceText};
return { result, triggers }
}
closedFunction()
`;
const scriptWithCallback = `
function callback (script) {
const userFunction = script;
const result = userFunction(CALLBACK_DATA);
return { result, triggers };
}
callback(${sourceText});
`;
const script = callbackData ? scriptWithCallback : scriptToEvaluate;
const data = callbackData
? { ...safeData, CALLBACK_DATA: safeCallbackData }
: safeData;
const { result, triggers } = this.rootRealm.evaluate(script, data);
return {
result: this.convertToMainScope(result),
triggers,
};
} catch (e) {
console.error(`Error: "${e.message}" when evaluating {{${sourceText}}}`);
return { result: undefined, triggers: [] };
}
return this.convertToMainScope(result);
}
}

View File

@ -6,6 +6,7 @@ import { Switch, Route } from "react-router-dom";
import { AppState } from "reducers";
import {
AppViewerRouteParams,
BuilderRouteParams,
getApplicationViewerPageURL,
} from "constants/routes";
import {
@ -18,7 +19,7 @@ import {
getIsInitialized,
} from "selectors/appViewSelectors";
import { executeAction } from "actions/widgetActions";
import { ActionPayload } from "constants/ActionConstants";
import { ExecuteActionPayload } from "constants/ActionConstants";
import SideNav from "./viewer/SideNav";
import { SideNavItemProps } from "./viewer/SideNavItem";
import AppViewerHeader from "./viewer/AppViewerHeader";
@ -27,7 +28,6 @@ import { RenderModes } from "constants/WidgetConstants";
import { EditorContext } from "components/editorComponents/EditorContextProvider";
import AppViewerPageContainer from "./AppViewerPageContainer";
import AppViewerSideNavWrapper from "./viewer/AppViewerSideNavWrapper";
import { PaginationField } from "api/ActionAPI";
import { updateWidgetMetaProperty } from "actions/metaActions";
const AppViewWrapper = styled.div`
@ -48,7 +48,7 @@ export type AppViewerProps = {
pages?: PageListPayload;
initializeAppViewer: Function;
isInitialized: boolean;
executeAction: (actionPayloads: ActionPayload[]) => void;
executeAction: (actionPayload: ExecuteActionPayload) => void;
updateWidgetProperty: (
widgetId: string,
propertyName: string,
@ -59,7 +59,7 @@ export type AppViewerProps = {
propertyName: string,
propertyValue: any,
) => void;
};
} & RouteComponentProps<BuilderRouteParams>;
class AppViewer extends Component<
AppViewerProps & RouteComponentProps<AppViewerRouteParams>
@ -70,6 +70,7 @@ class AppViewer extends Component<
this.props.initializeAppViewer(applicationId);
}
}
public render() {
const { isInitialized } = this.props;
if (!isInitialized) return null;
@ -121,10 +122,8 @@ const mapStateToProps = (state: AppState) => ({
});
const mapDispatchToProps = (dispatch: any) => ({
executeAction: (
actionPayloads: ActionPayload[],
paginationField?: PaginationField,
) => dispatch(executeAction(actionPayloads, paginationField)),
executeAction: (actionPayload: ExecuteActionPayload) =>
dispatch(executeAction(actionPayload)),
updateWidgetProperty: (
widgetId: string,
propertyName: string,

View File

@ -4,11 +4,10 @@ import {
ReduxAction,
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants";
import { ActionResponse, RestAction, PaginationField } from "api/ActionAPI";
import { ActionPayload, ExecuteErrorPayload } from "constants/ActionConstants";
import _ from "lodash";
import { ActionResponse, RestAction } from "api/ActionAPI";
import { ExecuteErrorPayload } from "constants/ActionConstants";
interface ActionData {
export interface ActionData {
isLoading: boolean;
config: RestAction;
data?: ActionResponse;
@ -67,15 +66,12 @@ const actionsReducer = createReducer(initialState, {
state: ActionDataState,
action: ReduxAction<{ id: string }>,
): ActionDataState => state.filter(a => a.config.id !== action.payload.id),
[ReduxActionTypes.EXECUTE_ACTION]: (
[ReduxActionTypes.EXECUTE_API_ACTION_REQUEST]: (
state: ActionDataState,
action: ReduxAction<{
actions: ActionPayload[];
paginationField: PaginationField;
}>,
action: ReduxAction<{ id: string }>,
): ActionDataState =>
state.map(a => {
if (_.find(action.payload.actions, { actionId: a.config.id })) {
if (a.config.id === action.payload.id) {
return {
...a,
isLoading: true,
@ -83,14 +79,13 @@ const actionsReducer = createReducer(initialState, {
}
return a;
}),
[ReduxActionTypes.EXECUTE_ACTION_SUCCESS]: (
[ReduxActionTypes.EXECUTE_API_ACTION_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<{ [id: string]: ActionResponse }>,
action: ReduxAction<{ id: string; response: ActionResponse }>,
): ActionDataState => {
const actionId = Object.keys(action.payload)[0];
return state.map(a => {
if (a.config.id === actionId) {
return { ...a, isLoading: false, data: action.payload[actionId] };
if (a.config.id === action.payload.id) {
return { ...a, isLoading: false, data: action.payload.response };
}
return a;
});

View File

@ -38,6 +38,7 @@ const canvasWidgetsReducer = createReducer(initialState, {
...widget,
[action.payload.propertyName]: action.payload.propertyValue,
dynamicBindings: action.payload.dynamicBindings,
dynamicTriggers: action.payload.dynamicTriggers,
},
};
},

View File

@ -57,5 +57,3 @@ export interface AppState {
meta: MetaState;
};
}
export type DataTree = AppState["entities"];

View File

@ -3,29 +3,27 @@ import {
ReduxActionErrorTypes,
ReduxActionTypes,
} from "constants/ReduxActionConstants";
import { Intent } from "@blueprintjs/core";
import {
all,
call,
select,
put,
select,
takeEvery,
takeLatest,
take,
} from "redux-saga/effects";
import {
ActionPayload,
EventType,
ExecuteActionPayload,
PageAction,
ExecuteJSActionPayload,
} from "constants/ActionConstants";
import ActionAPI, {
ActionApiResponse,
ActionCreateUpdateResponse,
ActionResponse,
ExecuteActionRequest,
PaginationField,
Property,
RestAction,
PaginationField,
} from "api/ActionAPI";
import { AppState } from "reducers";
import _ from "lodash";
@ -37,6 +35,8 @@ import {
copyActionSuccess,
createActionSuccess,
deleteActionSuccess,
executeApiActionRequest,
executeApiActionSuccess,
FetchActionsPayload,
moveActionError,
moveActionSuccess,
@ -50,17 +50,27 @@ import {
removeBindingsFromObject,
} from "utils/DynamicBindingUtils";
import { validateResponse } from "./ErrorSagas";
import {
ERROR_MESSAGE_SELECT_ACTION,
ERROR_MESSAGE_SELECT_ACTION_TYPE,
} from "constants/messages";
import { getFormData } from "selectors/formSelectors";
import { API_EDITOR_FORM_NAME } from "constants/forms";
import { executeAction, executeActionError } from "actions/widgetActions";
import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton";
import { getParsedDataTree } from "selectors/nameBindingsWithDataSelector";
import { getParsedDataTree } from "selectors/dataTreeSelectors";
import { transformRestAction } from "transformers/RestActionTransformer";
import { getActionResponses } from "selectors/entitiesSelector";
import {
ActionDescription,
RunActionPayload,
} from "entities/DataTree/dataTreeFactory";
import {
getCurrentApplicationId,
getPageList,
} from "selectors/editorSelectors";
import history from "utils/history";
import {
BUILDER_PAGE_URL,
getApplicationViewerPageURL,
} from "constants/routes";
import { ToastType } from "react-toastify";
export const getAction = (
state: AppState,
@ -89,7 +99,8 @@ const createActionErrorResponse = (
export function* evaluateDynamicBoundValueSaga(path: string): any {
const tree = yield select(getParsedDataTree);
return getDynamicValue(`{{${path}}}`, tree);
const dynamicResult = getDynamicValue(`{{${path}}}`, tree);
return dynamicResult.result;
}
export function* getActionParams(jsonPathKeys: string[] | undefined) {
@ -110,41 +121,40 @@ export function* getActionParams(jsonPathKeys: string[] | undefined) {
return mapToPropList(dynamicBindings);
}
function* executeJSActionSaga(jsAction: ExecuteJSActionPayload) {
const tree = yield select(getParsedDataTree);
const result = JSExecutionManagerSingleton.evaluateSync(
jsAction.jsFunction,
tree,
);
yield put({
type: ReduxActionTypes.SAVE_JS_EXECUTION_RECORD,
payload: {
[jsAction.jsFunctionId]: result,
},
});
}
// function* executeJSActionSaga(jsAction: ExecuteJSActionPayload) {
// const tree = yield select(getParsedDataTree);
// const result = JSExecutionManagerSingleton.evaluateSync(
// jsAction.jsFunction,
// tree,
// );
//
// yield put({
// type: ReduxActionTypes.SAVE_JS_EXECUTION_RECORD,
// payload: {
// [jsAction.jsFunctionId]: result,
// },
// });
// }
export function* executeAPIQueryActionSaga(
apiAction: ActionPayload,
paginationField: PaginationField,
apiAction: RunActionPayload,
event: EventType,
) {
const { actionId, onSuccess, onError } = apiAction;
try {
const api: PageAction = yield select(getAction, apiAction.actionId);
if (!api) {
yield put(
executeActionError({
actionId: apiAction.actionId,
error: "No action selected",
}),
);
return;
}
yield put(executeApiActionRequest({ id: apiAction.actionId }));
const api: RestAction = yield select(getAction, actionId);
const params: Property[] = yield call(getActionParams, api.jsonPathKeys);
const pagination =
event === EventType.ON_NEXT_PAGE
? "NEXT"
: event === EventType.ON_PREV_PAGE
? "PREV"
: undefined;
const executeActionRequest: ExecuteActionRequest = {
action: { id: apiAction.actionId },
action: { id: actionId },
params,
paginationField: paginationField,
paginationField: pagination,
};
const response: ActionApiResponse = yield ActionAPI.executeAction(
executeActionRequest,
@ -152,119 +162,113 @@ export function* executeAPIQueryActionSaga(
let payload = createActionResponse(response);
if (response.responseMeta && response.responseMeta.error) {
payload = createActionErrorResponse(response);
if (apiAction.onError) {
yield put({
type: ReduxActionTypes.EXECUTE_ACTION,
payload: {
actions: apiAction.onError,
},
});
if (onError) {
yield put(
executeAction({
dynamicString: onError,
event: {
type: EventType.ON_ERROR,
},
responseData: payload,
}),
);
}
yield put(
executeActionError({
actionId: apiAction.actionId,
actionId,
error: response.responseMeta.error,
}),
);
} else {
if (apiAction.onSuccess) {
yield put({
type: ReduxActionTypes.EXECUTE_ACTION,
payload: {
actions: apiAction.onSuccess,
},
});
yield put(
executeApiActionSuccess({ id: apiAction.actionId, response: payload }),
);
if (onSuccess) {
yield put(
executeAction({
dynamicString: onSuccess,
event: {
type: EventType.ON_SUCCESS,
},
responseData: payload,
}),
);
}
yield put({
type: ReduxActionTypes.EXECUTE_ACTION_SUCCESS,
payload: { [apiAction.actionId]: payload },
});
}
return response;
} catch (error) {
yield put(
executeActionError({
actionId: apiAction.actionId,
actionId: actionId,
error,
}),
);
if (onError) {
yield put(
executeAction({
dynamicString: `{{${onError}}}`,
event: {
type: EventType.ON_ERROR,
},
responseData: {},
}),
);
}
}
}
function validateActionPayload(actionPayload: ActionPayload) {
const validation = {
isValid: true,
messages: [] as string[],
};
const noActionId = actionPayload.actionId === undefined;
validation.isValid = validation.isValid && !noActionId;
if (noActionId) {
validation.messages.push(ERROR_MESSAGE_SELECT_ACTION);
function* navigateActionSaga(action: { pageName: string }, event: EventType) {
const pageList = yield select(getPageList);
const applicationId = yield select(getCurrentApplicationId);
const page = _.find(pageList, { pageName: action.pageName });
if (page) {
// TODO need to make this check via RENDER_MODE;
const path = history.location.pathname.endsWith("/edit")
? BUILDER_PAGE_URL(applicationId, page.pageId)
: getApplicationViewerPageURL(applicationId, page.pageId);
history.push(path);
}
const noActionType = actionPayload.actionType === undefined;
validation.isValid = validation.isValid && !noActionType;
if (noActionType) {
validation.messages.push(ERROR_MESSAGE_SELECT_ACTION_TYPE);
}
return validation;
}
export function* executeActionSaga(
actionPayloads: ActionPayload[],
paginationField: PaginationField,
): any {
yield all(
_.map(actionPayloads, (actionPayload: ActionPayload) => {
const actionValidation = validateActionPayload(actionPayload);
if (!actionValidation.isValid) {
console.error(actionValidation.messages.join(", "));
return undefined;
}
switch (actionPayload.actionType) {
case "API":
return call(
executeAPIQueryActionSaga,
actionPayload,
paginationField,
);
case "QUERY":
return call(
executeAPIQueryActionSaga,
actionPayload,
paginationField,
);
case "JS_FUNCTION":
return call(
executeJSActionSaga,
actionPayload as ExecuteJSActionPayload,
);
default:
return undefined;
}
}),
);
}
export function* executeReduxActionSaga(
action: ReduxAction<{
actions: ActionPayload[];
paginationField: PaginationField;
}>,
export function* executeActionTriggers(
trigger: ActionDescription<any>,
event: EventType,
) {
if (!_.isNil(action.payload)) {
yield* executeActionSaga(
action.payload.actions,
action.payload.paginationField,
);
} else {
yield put(
executeActionError({
actionId: "",
error: "No action payload",
}),
switch (trigger.type) {
case "RUN_ACTION":
yield call(executeAPIQueryActionSaga, trigger.payload, event);
break;
case "NAVIGATE_TO":
yield call(navigateActionSaga, trigger.payload, event);
break;
case "NAVIGATE_TO_URL":
if (trigger.payload.url) {
window.location.href = trigger.payload.url;
}
break;
case "SHOW_ALERT":
AppToaster.show({
message: trigger.payload.message,
type: trigger.payload.style,
});
break;
default:
yield put(
executeActionError({
error: "Trigger type unknown",
actionId: "",
}),
);
}
}
export function* executeAppAction(action: ReduxAction<ExecuteActionPayload>) {
const { dynamicString, event, responseData } = action.payload;
const tree = yield select(getParsedDataTree);
const { triggers } = getDynamicValue(dynamicString, tree, responseData, true);
if (triggers) {
yield all(
triggers.map(trigger => call(executeActionTriggers, trigger, event.type)),
);
}
}
@ -278,7 +282,7 @@ export function* createActionSaga(actionPayload: ReduxAction<RestAction>) {
if (isValidResponse) {
AppToaster.show({
message: `${actionPayload.payload.name} Action created`,
intent: Intent.SUCCESS,
type: ToastType.SUCCESS,
});
yield put(createActionSuccess(response.data));
}
@ -324,7 +328,7 @@ export function* updateActionSaga(
if (isValidResponse) {
AppToaster.show({
message: `${actionPayload.payload.data.name} Action updated`,
intent: Intent.SUCCESS,
type: ToastType.SUCCESS,
});
yield put(updateActionSuccess({ data: response.data }));
yield put(runApiAction(data.id));
@ -347,7 +351,7 @@ export function* deleteActionSaga(actionPayload: ReduxAction<{ id: string }>) {
if (isValidResponse) {
AppToaster.show({
message: `${response.data.name} Action deleted`,
intent: Intent.SUCCESS,
type: ToastType.SUCCESS,
});
yield put(deleteActionSuccess({ id }));
}
@ -421,12 +425,11 @@ export function* runApiActionSaga(
function* executePageLoadActionsSaga(action: ReduxAction<PageAction[][]>) {
const pageActions = action.payload;
const actionPayloads: ActionPayload[][] = pageActions.map(actionSet =>
const actionPayloads: RunActionPayload[][] = pageActions.map(actionSet =>
actionSet.map(action => ({
actionId: action.id,
actionType: action.pluginType,
contextParams: {},
actionName: action.name,
onSuccess: "",
onError: "",
})),
);
for (const actionSet of actionPayloads) {
@ -434,11 +437,11 @@ function* executePageLoadActionsSaga(action: ReduxAction<PageAction[][]>) {
const filteredSet = actionSet.filter(
action => !apiResponses[action.actionId],
);
yield put(executeAction(filteredSet));
/* eslint-disable no-empty-pattern */
for (const {} of filteredSet) {
yield take(ReduxActionTypes.EXECUTE_ACTION_SUCCESS);
}
yield* yield all(
filteredSet.map(a =>
call(executeAPIQueryActionSaga, a, EventType.ON_PAGE_LOAD),
),
);
}
}
@ -469,14 +472,14 @@ function* moveActionSaga(
if (isValidResponse) {
AppToaster.show({
message: `${response.data.name} Action moved`,
intent: Intent.SUCCESS,
type: ToastType.SUCCESS,
});
}
yield put(moveActionSuccess(response.data));
} catch (e) {
AppToaster.show({
message: `Error while moving action ${actionObject.name}`,
intent: Intent.DANGER,
type: ToastType.ERROR,
});
yield put(
moveActionError({
@ -510,14 +513,14 @@ function* copyActionSaga(
if (isValidResponse) {
AppToaster.show({
message: `${actionObject.name} Action copied`,
intent: Intent.SUCCESS,
type: ToastType.SUCCESS,
});
}
yield put(copyActionSuccess(response.data));
} catch (e) {
AppToaster.show({
message: `Error while copying action ${actionObject.name}`,
intent: Intent.DANGER,
type: ToastType.ERROR,
});
yield put(copyActionError(action.payload));
}
@ -526,7 +529,7 @@ function* copyActionSaga(
export function* watchActionSagas() {
yield all([
takeEvery(ReduxActionTypes.FETCH_ACTIONS_INIT, fetchActionsSaga),
takeLatest(ReduxActionTypes.EXECUTE_ACTION, executeReduxActionSaga),
takeLatest(ReduxActionTypes.EXECUTE_ACTION, executeAppAction),
takeLatest(ReduxActionTypes.RUN_API_REQUEST, runApiActionSaga),
takeLatest(ReduxActionTypes.CREATE_ACTION_INIT, createActionSaga),
takeLatest(ReduxActionTypes.UPDATE_ACTION_INIT, updateActionSaga),

View File

@ -1,5 +1,4 @@
import _ from "lodash";
import { Intent } from "@blueprintjs/core";
import {
ReduxActionTypes,
ReduxActionErrorTypes,
@ -10,6 +9,7 @@ import { DEFAULT_ERROR_MESSAGE, DEFAULT_ACTION_ERROR } from "constants/errors";
import { ApiResponse } from "api/ApiResponses";
import { put, takeLatest, call } from "redux-saga/effects";
import { ERROR_500 } from "constants/messages";
import { ToastType } from "react-toastify";
export function* callAPI(apiCall: any, requestPayload: any) {
try {
@ -80,7 +80,7 @@ export function* errorSaga(
payload: { error, show = true },
} = errorAction;
const message = error.message || ActionErrorDisplayMap[type](error);
if (show) AppToaster.show({ message, intent: Intent.DANGER });
if (show) AppToaster.show({ message, type: ToastType.ERROR });
yield put({
type: ReduxActionTypes.REPORT_ERROR,
payload: {

View File

@ -22,6 +22,7 @@ import { isDynamicValue } from "utils/DynamicBindingUtils";
import { WidgetProps } from "widgets/BaseWidget";
import _ from "lodash";
import { WidgetTypes } from "constants/WidgetConstants";
import WidgetFactory from "utils/WidgetFactory";
export function* addChildSaga(addChildAction: ReduxAction<WidgetAddChild>) {
try {
@ -184,15 +185,29 @@ function* updateWidgetPropertySaga(
const isDynamic = isDynamicValue(propertyValue);
const widget: WidgetProps = yield select(getWidget, widgetId);
let dynamicBindings: Record<string, boolean> = widget.dynamicBindings || {};
if (!isDynamic && propertyName in dynamicBindings) {
dynamicBindings = _.omit(dynamicBindings, propertyName);
}
if (isDynamic && !(propertyName in dynamicBindings)) {
dynamicBindings[propertyName] = true;
let dynamicTriggers: Record<string, true> = widget.dynamicTriggers || {};
const triggerProperties = WidgetFactory.getWidgetTriggerPropertiesMap(
widget.type,
);
if (propertyName in triggerProperties) {
if (propertyValue && !(propertyName in dynamicTriggers)) {
dynamicTriggers[propertyName] = true;
}
if (!propertyValue && propertyName in dynamicTriggers) {
dynamicTriggers = _.omit(dynamicTriggers, propertyName);
}
} else {
if (!isDynamic && propertyName in dynamicBindings) {
dynamicBindings = _.omit(dynamicBindings, propertyName);
}
if (isDynamic && !(propertyName in dynamicBindings)) {
dynamicBindings[propertyName] = true;
}
}
yield put({
type: ReduxActionTypes.UPDATE_WIDGET_PROPERTY,
payload: { ...updateAction.payload, dynamicBindings },
payload: { ...updateAction.payload, dynamicBindings, dynamicTriggers },
});
}

View File

@ -1,12 +1,11 @@
import { createSelector } from "reselect";
import { AppState, DataTree } from "reducers";
import { AppState } from "reducers";
import { AppViewReduxState } from "reducers/uiReducers/appViewReducer";
import { PageListReduxState } from "reducers/entityReducers/pageListReducer";
import { getDataTree } from "./entitiesSelector";
import { getEntities } from "./entitiesSelector";
import createCachedSelector from "re-reselect";
import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer";
import { getValidatedDynamicProps } from "./editorSelectors";
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import { getValidatedWidgetsAndActionTriggers } from "./editorSelectors";
const getAppViewState = (state: AppState) => state.ui.appView;
const getPageListState = (state: AppState): PageListReduxState =>
@ -49,16 +48,11 @@ export const getPageWidgetId = createSelector(
);
export const getCurrentPageLayoutDSL = createCachedSelector(
getPageWidgetId,
getDataTree,
getValidatedDynamicProps,
(
pageWidgetId: string,
entities: DataTree,
validatedDynamicWidgets: CanvasWidgetsReduxState,
) => {
getEntities,
getValidatedWidgetsAndActionTriggers,
(pageWidgetId: string, entities: AppState["entities"], widgets) => {
return CanvasWidgetsNormalizer.denormalize(pageWidgetId, {
...entities,
canvasWidgets: validatedDynamicWidgets,
canvasWidgets: widgets,
});
},
)((pageWidgetId, entities) => entities || 0);

View File

@ -0,0 +1,46 @@
import { AppState } from "reducers";
import { createSelector } from "reselect";
import { getActions } from "./entitiesSelector";
import { ActionDataState } from "reducers/entityReducers/actionsReducer";
import createCachedSelector from "re-reselect";
import { getEvaluatedDataTree } from "utils/DynamicBindingUtils";
import { extraLibraries } from "jsExecution/JSExecutionManagerSingleton";
import { DataTree, DataTreeFactory } from "entities/DataTree/dataTreeFactory";
export const getUnevaluatedDataTree = (state: AppState): DataTree =>
DataTreeFactory.create(state);
export const getParsedDataTree = createSelector(
getUnevaluatedDataTree,
(dataTree: DataTree) => {
return getEvaluatedDataTree(dataTree, true);
},
);
// For autocomplete. Use actions cached responses if
// there isn't a response already
export const getDataTreeForAutocomplete = createCachedSelector(
getParsedDataTree,
getActions,
(dataTree: DataTree, actions: ActionDataState) => {
const cachedResponses: Record<string, any> = {};
if (actions && actions.length) {
actions.forEach(action => {
if (!(action.config.name in dataTree) && action.config.cacheResponse) {
try {
cachedResponses[action.config.name] = JSON.parse(
action.config.cacheResponse,
);
} catch (e) {
cachedResponses[action.config.name] = action.config.cacheResponse;
}
}
});
}
const libs: Record<string, any> = {};
extraLibraries.forEach(
config => (libs[config.accessor] = libs[config.accessor]),
);
return { ...dataTree, ...cachedResponses, ...libs };
},
)((state: AppState) => state.entities.actions.length);

View File

@ -1,13 +1,13 @@
import { createSelector } from "reselect";
import createCachedSelector from "re-reselect";
import { AppState, DataTree } from "reducers";
import { AppState } from "reducers";
import { EditorReduxState } from "reducers/uiReducers/editorReducer";
import { WidgetConfigReducerState } from "reducers/entityReducers/widgetConfigReducer";
import { WidgetCardProps } from "widgets/BaseWidget";
import { WidgetSidebarReduxState } from "reducers/uiReducers/widgetSidebarReducer";
import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer";
import { getDataTree } from "./entitiesSelector";
import { getEntities } from "./entitiesSelector";
import {
FlattenedWidgetProps,
CanvasWidgetsReduxState,
@ -16,7 +16,7 @@ import { PageListReduxState } from "reducers/entityReducers/pageListReducer";
import { OccupiedSpace } from "constants/editorConstants";
import { WidgetTypes } from "constants/WidgetConstants";
import { getParsedDataTree } from "./nameBindingsWithDataSelector";
import { getParsedDataTree } from "selectors/dataTreeSelectors";
import _ from "lodash";
const getEditorState = (state: AppState) => state.ui.editor;
@ -116,10 +116,10 @@ export const getWidgetCards = createSelector(
},
);
export const getValidatedDynamicProps = createSelector(
getDataTree,
export const getValidatedWidgetsAndActionTriggers = createSelector(
getEntities,
getParsedDataTree,
(entities: DataTree, tree) => {
(entities: AppState["entities"], tree) => {
const widgets = { ...entities.canvasWidgets };
Object.keys(widgets).forEach(widgetKey => {
const evaluatedWidget = _.find(tree, { widgetId: widgetKey });
@ -140,7 +140,7 @@ export const getValidatedDynamicProps = createSelector(
export const getDenormalizedDSL = createCachedSelector(
getPageWidgetId,
getValidatedDynamicProps,
getValidatedWidgetsAndActionTriggers,
(pageWidgetId: string, validatedDynamicWidgets: CanvasWidgetsReduxState) => {
return CanvasWidgetsNormalizer.denormalize(pageWidgetId, {
canvasWidgets: validatedDynamicWidgets,

View File

@ -1,8 +1,9 @@
import { AppState, DataTree } from "reducers";
import { AppState } from "reducers";
import { ActionDataState } from "reducers/entityReducers/actionsReducer";
import { ActionResponse } from "api/ActionAPI";
export const getDataTree = (state: AppState): DataTree => state.entities;
export const getEntities = (state: AppState): AppState["entities"] =>
state.entities;
export const getPluginIdOfName = (
state: AppState,

View File

@ -1,72 +0,0 @@
import { AppState, DataTree } from "reducers";
import { createSelector } from "reselect";
import { getActions, getDataTree } from "./entitiesSelector";
import { ActionDataState } from "reducers/entityReducers/actionsReducer";
import createCachedSelector from "re-reselect";
import { getEvaluatedDataTree } from "utils/DynamicBindingUtils";
import {
ENTITY_TYPE_ACTION,
ENTITY_TYPE_WIDGET,
} from "constants/entityConstants";
import { extraLibraries } from "jsExecution/JSExecutionManagerSingleton";
export type NameBindingsWithData = { [key: string]: any };
export const getNameBindingsWithData = createSelector(
getDataTree,
(dataTree: DataTree): NameBindingsWithData => {
const nameBindingsWithData: Record<string, object> = {};
dataTree.actions.forEach(a => {
nameBindingsWithData[a.config.name] = {
...a,
data: a.data ? a.data.body : {},
__type: ENTITY_TYPE_ACTION,
};
});
Object.keys(dataTree.canvasWidgets).forEach(w => {
const widget = dataTree.canvasWidgets[w];
const widgetMetaProps = dataTree.meta[w];
nameBindingsWithData[widget.widgetName] = {
...widget,
...widgetMetaProps,
__type: ENTITY_TYPE_WIDGET,
};
});
return nameBindingsWithData;
},
);
export const getParsedDataTree = createSelector(
getNameBindingsWithData,
(namedBindings: NameBindingsWithData) => {
return getEvaluatedDataTree(namedBindings, true);
},
);
// For autocomplete. Use actions cached responses if
// there isn't a response already
export const getNameBindingsForAutocomplete = createCachedSelector(
getParsedDataTree,
getActions,
(dataTree: NameBindingsWithData, actions: ActionDataState) => {
const cachedResponses: Record<string, any> = {};
if (actions && actions.length) {
actions.forEach(action => {
if (!(action.config.name in dataTree) && action.config.cacheResponse) {
try {
cachedResponses[action.config.name] = JSON.parse(
action.config.cacheResponse,
);
} catch (e) {
cachedResponses[action.config.name] = action.config.cacheResponse;
}
}
});
}
const libs: Record<string, any> = {};
extraLibraries.forEach(
config => (libs[config.accessor] = libs[config.accessor]),
);
return { ...dataTree, ...cachedResponses, ...libs };
},
)((state: AppState) => state.entities.actions.length);

View File

@ -9,11 +9,9 @@ import {
getEvaluatedDataTree,
} from "utils/DynamicBindingUtils";
import { WidgetProps } from "widgets/BaseWidget";
import {
NameBindingsWithData,
getNameBindingsWithData,
} from "./nameBindingsWithDataSelector";
import { getUnevaluatedDataTree } from "selectors/dataTreeSelectors";
import _ from "lodash";
import { DataTree } from "entities/DataTree/dataTreeFactory";
const getPropertyPaneState = (state: AppState): PropertyPaneReduxState =>
state.ui.propertyPane;
@ -42,11 +40,8 @@ export const getCurrentWidgetProperties = createSelector(
export const getWidgetPropsWithValidations = createSelector(
getCurrentWidgetProperties,
getNameBindingsWithData,
(
widget: WidgetProps | undefined,
nameBindingsWithData: NameBindingsWithData,
) => {
getUnevaluatedDataTree,
(widget: WidgetProps | undefined, nameBindingsWithData: DataTree) => {
if (!widget) return undefined;
const tree = getEvaluatedDataTree(nameBindingsWithData, false);
const evaluatedWidget = _.find(tree, { widgetId: widget.widgetId });

View File

@ -2,11 +2,17 @@ import _ from "lodash";
import { WidgetProps } from "widgets/BaseWidget";
import { DATA_BIND_REGEX } from "constants/BindingsConstants";
import ValidationFactory from "./ValidationFactory";
import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton";
import JSExecutionManagerSingleton, {
JSExecutorResult,
} from "jsExecution/JSExecutionManagerSingleton";
import unescapeJS from "unescape-js";
import { NameBindingsWithData } from "selectors/nameBindingsWithDataSelector";
import toposort from "toposort";
import { ENTITY_TYPE_ACTION } from "constants/entityConstants";
import {
DataTree,
DataTreeAction,
DataTreeWidget,
ENTITY_TYPE,
} from "entities/DataTree/dataTreeFactory";
export const removeBindingsFromObject = (obj: object) => {
const string = JSON.stringify(obj);
@ -100,12 +106,18 @@ export const getDynamicBindings = (
};
// Paths are expected to have "{name}.{path}" signature
// Also returns any action triggers found after evaluating value
export const evaluateDynamicBoundValue = (
data: NameBindingsWithData,
data: DataTree,
path: string,
): any => {
callbackData?: any,
): JSExecutorResult => {
const unescapedInput = unescapeJS(path);
return JSExecutionManagerSingleton.evaluateSync(unescapedInput, data);
return JSExecutionManagerSingleton.evaluateSync(
unescapedInput,
data,
callbackData,
);
};
// For creating a final value where bindings could be in a template format
@ -128,26 +140,40 @@ export const createDynamicValueString = (
export const getDynamicValue = (
dynamicBinding: string,
data: NameBindingsWithData,
): any => {
data: DataTree,
callBackData?: any,
includeTriggers = false,
): JSExecutorResult => {
// Get the {{binding}} bound values
const { bindings, paths } = getDynamicBindings(dynamicBinding);
if (bindings.length) {
// Get the Data Tree value of those "binding "paths
const values = paths.map((p, i) => {
if (p) {
return evaluateDynamicBoundValue(data, p);
const result = evaluateDynamicBoundValue(data, p, callBackData);
if (includeTriggers) {
return result;
} else {
return { result: result.result };
}
} else {
return bindings[i];
return { result: bindings[i], triggers: [] };
}
});
// if it is just one binding, no need to create template string
if (bindings.length === 1) return values[0];
// else return a string template with bindings
return createDynamicValueString(dynamicBinding, bindings, values);
const templateString = createDynamicValueString(
dynamicBinding,
bindings,
values.map(v => v.result),
);
return {
result: templateString,
};
}
return undefined;
return { result: undefined, triggers: [] };
};
export const enhanceWidgetWithValidations = (
@ -199,7 +225,7 @@ export const getParsedTree = (tree: any) => {
};
export const getEvaluatedDataTree = (
dataTree: NameBindingsWithData,
dataTree: DataTree,
parseValues: boolean,
) => {
const dynamicDependencyMap = createDependencyTree(dataTree);
@ -218,7 +244,7 @@ export const getEvaluatedDataTree = (
type DynamicDependencyMap = Record<string, Array<string>>;
export const createDependencyTree = (
dataTree: NameBindingsWithData,
dataTree: DataTree,
): Array<[string, string]> => {
const dependencyMap: DynamicDependencyMap = {};
const allKeys = getAllPaths(dataTree);
@ -276,20 +302,24 @@ const calculateSubDependencies = (
};
export const setTreeLoading = (
dataTree: NameBindingsWithData,
dataTree: DataTree,
dependencyMap: Array<[string, string]>,
) => {
const result = _.cloneDeep(dataTree);
Object.keys(dataTree)
.filter(
e => dataTree[e].__type === ENTITY_TYPE_ACTION && dataTree[e].isLoading,
)
.filter(e => {
const entity = dataTree[e] as DataTreeAction;
return entity.ENTITY_TYPE === ENTITY_TYPE.ACTION && entity.isLoading;
})
.reduce(
(allEntities: string[], curr) =>
allEntities.concat(getEntityDependencies(dependencyMap, curr)),
[],
)
.forEach(w => (result[w].isLoading = true));
.forEach(w => {
const entity = result[w] as DataTreeWidget;
entity.isLoading = true;
});
return result;
};
@ -329,10 +359,10 @@ export const getEntityDependencies = (
};
export function dependencySortedEvaluateDataTree(
dataTree: NameBindingsWithData,
dataTree: DataTree,
dependencyTree: Array<[string, string]>,
parseValues: boolean,
) {
): DataTree {
const tree = _.cloneDeep(dataTree);
try {
// sort dependencies and remove empty dependencies
@ -340,30 +370,28 @@ export function dependencySortedEvaluateDataTree(
.reverse()
.filter(d => !!d);
// evaluate and replace values
return sortedDependencies.reduce(
(currentTree: NameBindingsWithData, path: string) => {
const binding = _.get(currentTree as any, path);
const widgetType = _.get(
currentTree as any,
`${path.split(".")[0]}.type`,
null,
return sortedDependencies.reduce((currentTree: DataTree, path: string) => {
const binding = _.get(currentTree as any, path);
const widgetType = _.get(
currentTree as any,
`${path.split(".")[0]}.type`,
null,
);
let result = binding;
if (isDynamicValue(binding)) {
const dynamicResult = getDynamicValue(binding, currentTree);
result = dynamicResult.result;
}
if (widgetType && parseValues) {
const { parsed } = ValidationFactory.validateWidgetProperty(
widgetType,
`${path.split(".")[1]}`,
result,
);
let result = binding;
if (isDynamicValue(binding)) {
result = getDynamicValue(binding, currentTree);
}
if (widgetType && parseValues) {
const { parsed } = ValidationFactory.validateWidgetProperty(
widgetType,
`${path.split(".")[1]}`,
result,
);
result = parsed;
}
return _.set(currentTree, path, result);
},
tree,
);
result = parsed;
}
return _.set(currentTree, path, result);
}, tree);
} catch (e) {
console.error(e);
return tree;

View File

@ -3,19 +3,19 @@ import {
mockExecute,
mockRegisterLibrary,
} from "../../test/__mocks__/RealmExecutorMock";
jest.mock("jsExecution/RealmExecutor", () => {
return jest.fn().mockImplementation(() => {
return { execute: mockExecute, registerLibrary: mockRegisterLibrary };
});
});
import {
dependencySortedEvaluateDataTree,
getDynamicValue,
getEntityDependencies,
parseDynamicString,
} from "./DynamicBindingUtils";
import { getNameBindingsWithData } from "selectors/nameBindingsWithDataSelector";
import { AppState, DataTree } from "reducers";
import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
jest.mock("jsExecution/RealmExecutor", () => {
return jest.fn().mockImplementation(() => {
return { execute: mockExecute, registerLibrary: mockRegisterLibrary };
});
});
beforeAll(() => {
mockRegisterLibrary.mockClear();
@ -24,12 +24,24 @@ beforeAll(() => {
it("Gets the value from the data tree", () => {
const dynamicBinding = "{{GetUsers.data}}";
const nameBindingsWithData = {
const nameBindingsWithData: DataTree = {
GetUsers: {
data: "correct data",
data: { text: "correct data" },
config: {
id: "id",
name: "text",
actionConfiguration: {},
pageId: "",
jsonPathKeys: [],
datasource: { id: "" },
pluginType: "1",
},
isLoading: false,
ENTITY_TYPE: ENTITY_TYPE.ACTION,
run: jest.fn(),
},
};
const actualValue = "correct data";
const actualValue = { result: { text: "correct data" } };
const value = getDynamicValue(dynamicBinding, nameBindingsWithData);
@ -101,7 +113,7 @@ it("evaluates the data tree", () => {
},
};
const result = dependencySortedEvaluateDataTree(input, dynamicBindings);
const result = dependencySortedEvaluateDataTree(input, dynamicBindings, true);
expect(result).toEqual(output);
});

View File

@ -73,11 +73,7 @@ class PropertyControlRegistry {
});
PropertyControlFactory.registerControlBuilder("COLUMN_ACTION_SELECTOR", {
buildPropertyControl(controlProps: ControlProps): JSX.Element {
return (
<ColumnActionSelectorControl
{...controlProps}
></ColumnActionSelectorControl>
);
return <ColumnActionSelectorControl {...controlProps} />;
},
});
}

View File

@ -8,6 +8,7 @@ import { WidgetPropertyValidationType } from "./ValidationFactory";
type WidgetDerivedPropertyType = any;
export type DerivedPropertiesMap = Record<string, string>;
export type TriggerPropertiesMap = Record<string, true>;
class WidgetFactory {
static widgetMap: Map<WidgetType, WidgetBuilder<WidgetProps>> = new Map();
@ -23,16 +24,22 @@ class WidgetFactory {
WidgetType,
DerivedPropertiesMap
> = new Map();
static triggerPropertiesMap: Map<
WidgetType,
TriggerPropertiesMap
> = new Map();
static registerWidgetBuilder(
widgetType: WidgetType,
widgetBuilder: WidgetBuilder<WidgetProps>,
widgetPropertyValidation: WidgetPropertyValidationType,
derivedPropertiesMap: DerivedPropertiesMap,
triggerPropertiesMap: TriggerPropertiesMap,
) {
this.widgetMap.set(widgetType, widgetBuilder);
this.widgetPropValidationMap.set(widgetType, widgetPropertyValidation);
this.derivedPropertiesMap.set(widgetType, derivedPropertiesMap);
this.triggerPropertiesMap.set(widgetType, triggerPropertiesMap);
}
static createWidget(
@ -84,6 +91,17 @@ class WidgetFactory {
}
return map;
}
static getWidgetTriggerPropertiesMap(
widgetType: WidgetType,
): TriggerPropertiesMap {
const map = this.triggerPropertiesMap.get(widgetType);
if (!map) {
console.error("Widget trigger map is not defined");
return {};
}
return map;
}
}
export interface WidgetCreationException {

View File

@ -33,6 +33,7 @@ class WidgetBuilderRegistry {
},
ContainerWidget.getPropertyValidationMap(),
ContainerWidget.getDerivedPropertiesMap(),
ContainerWidget.getTriggerPropertyMap(),
);
WidgetFactory.registerWidgetBuilder(
@ -44,6 +45,7 @@ class WidgetBuilderRegistry {
},
TextWidget.getPropertyValidationMap(),
TextWidget.getDerivedPropertiesMap(),
TextWidget.getTriggerPropertyMap(),
);
WidgetFactory.registerWidgetBuilder(
@ -55,6 +57,7 @@ class WidgetBuilderRegistry {
},
ButtonWidget.getPropertyValidationMap(),
ButtonWidget.getDerivedPropertiesMap(),
ButtonWidget.getTriggerPropertyMap(),
);
WidgetFactory.registerWidgetBuilder(
@ -66,6 +69,7 @@ class WidgetBuilderRegistry {
},
SpinnerWidget.getPropertyValidationMap(),
SpinnerWidget.getDerivedPropertiesMap(),
SpinnerWidget.getTriggerPropertyMap(),
);
WidgetFactory.registerWidgetBuilder(
@ -77,6 +81,7 @@ class WidgetBuilderRegistry {
},
InputWidget.getPropertyValidationMap(),
InputWidget.getDerivedPropertiesMap(),
InputWidget.getTriggerPropertyMap(),
);
WidgetFactory.registerWidgetBuilder(
@ -88,6 +93,7 @@ class WidgetBuilderRegistry {
},
CheckboxWidget.getPropertyValidationMap(),
CheckboxWidget.getDerivedPropertiesMap(),
ContainerWidget.getTriggerPropertyMap(),
);
WidgetFactory.registerWidgetBuilder(
@ -99,6 +105,7 @@ class WidgetBuilderRegistry {
},
DropdownWidget.getPropertyValidationMap(),
DropdownWidget.getDerivedPropertiesMap(),
DropdownWidget.getTriggerPropertyMap(),
);
WidgetFactory.registerWidgetBuilder(
@ -110,6 +117,7 @@ class WidgetBuilderRegistry {
},
RadioGroupWidget.getPropertyValidationMap(),
RadioGroupWidget.getDerivedPropertiesMap(),
RadioGroupWidget.getTriggerPropertyMap(),
);
WidgetFactory.registerWidgetBuilder(
@ -121,6 +129,7 @@ class WidgetBuilderRegistry {
},
ImageWidget.getPropertyValidationMap(),
ImageWidget.getDerivedPropertiesMap(),
ImageWidget.getTriggerPropertyMap(),
);
WidgetFactory.registerWidgetBuilder(
"TABLE_WIDGET",
@ -131,6 +140,7 @@ class WidgetBuilderRegistry {
},
TableWidget.getPropertyValidationMap(),
TableWidget.getDerivedPropertiesMap(),
TableWidget.getTriggerPropertyMap(),
);
WidgetFactory.registerWidgetBuilder(
"FILE_PICKER_WIDGET",
@ -141,6 +151,7 @@ class WidgetBuilderRegistry {
},
FilePickerWidget.getPropertyValidationMap(),
FilePickerWidget.getDerivedPropertiesMap(),
FilePickerWidget.getTriggerPropertyMap(),
);
WidgetFactory.registerWidgetBuilder(
"DATE_PICKER_WIDGET",
@ -151,6 +162,7 @@ class WidgetBuilderRegistry {
},
DatePickerWidget.getPropertyValidationMap(),
DatePickerWidget.getDerivedPropertiesMap(),
DatePickerWidget.getTriggerPropertyMap(),
);
}
}

View File

@ -1,7 +1,5 @@
import React, { Component } from "react";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import { ActionPayload } from "constants/ActionConstants";
import { WidgetProps } from "./BaseWidget";
class AlertWidget extends Component {
getPageView() {
@ -17,7 +15,6 @@ export interface AlertWidgetProps extends WidgetProps {
intent: MessageIntent;
header: string;
message: string;
onPrimaryClick: ActionPayload[];
}
export default AlertWidget;

View File

@ -19,7 +19,7 @@ import {
import _ from "lodash";
import DraggableComponent from "components/editorComponents/DraggableComponent";
import ResizableComponent from "components/editorComponents/ResizableComponent";
import { ActionPayload } from "constants/ActionConstants";
import { ExecuteActionPayload } from "constants/ActionConstants";
import PositionedContainer from "components/designSystems/appsmith/PositionedContainer";
import WidgetNameComponent from "components/designSystems/appsmith/WidgetNameComponent";
import shallowequal from "shallowequal";
@ -28,8 +28,10 @@ import { PositionTypes } from "constants/WidgetConstants";
import ErrorBoundary from "components/editorComponents/ErrorBoundry";
import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { DerivedPropertiesMap } from "utils/WidgetFactory";
import { PaginationField } from "api/ActionAPI";
import {
DerivedPropertiesMap,
TriggerPropertiesMap,
} from "utils/WidgetFactory";
/***
* BaseWidget
*
@ -70,6 +72,10 @@ abstract class BaseWidget<
return {};
}
static getTriggerPropertyMap(): TriggerPropertiesMap {
return {};
}
/**
* Widget abstraction to register the widget type
* ```javascript
@ -84,14 +90,9 @@ abstract class BaseWidget<
* Widgets can execute actions using this `executeAction` method.
* Triggers may be specific to the widget
*/
executeAction(
actionPayloads?: ActionPayload[],
paginationField?: PaginationField,
): void {
executeAction(actionPayload: ExecuteActionPayload): void {
const { executeAction } = this.context;
executeAction &&
!_.isNil(actionPayloads) &&
executeAction(actionPayloads, paginationField);
executeAction && executeAction(actionPayload);
}
disableDrag(disable: boolean) {
@ -269,6 +270,7 @@ export interface WidgetProps extends WidgetDataProps {
key?: string;
renderMode: RenderMode;
dynamicBindings?: Record<string, boolean>;
dynamicTriggers?: Record<string, true>;
invalidProps?: Record<string, boolean>;
validationMessages?: Record<string, string>;
isDefaultClickDisabled?: boolean;

View File

@ -2,9 +2,10 @@ import React from "react";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import ButtonComponent from "components/designSystems/blueprint/ButtonComponent";
import { ActionPayload } from "constants/ActionConstants";
import { EventType } from "constants/ActionConstants";
import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
import { TriggerPropertiesMap } from "utils/WidgetFactory";
class ButtonWidget extends BaseWidget<ButtonWidgetProps, WidgetState> {
onButtonClickBound: (event: React.MouseEvent<HTMLElement>) => void;
@ -23,8 +24,21 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, WidgetState> {
};
}
static getTriggerPropertyMap(): TriggerPropertiesMap {
return {
onClick: true,
};
}
onButtonClick() {
super.executeAction(this.props.onClick);
if (this.props.onClick) {
super.executeAction({
dynamicString: this.props.onClick,
event: {
type: EventType.ON_CLICK,
},
});
}
}
getPageView() {
@ -56,7 +70,7 @@ export type ButtonStyle =
export interface ButtonWidgetProps extends WidgetProps {
text?: string;
buttonStyle?: ButtonStyle;
onClick?: ActionPayload[];
onClick?: string;
isDisabled?: boolean;
isVisible?: boolean;
}

View File

@ -2,9 +2,10 @@ import React from "react";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import CheckboxComponent from "components/designSystems/blueprint/CheckboxComponent";
import { ActionPayload } from "constants/ActionConstants";
import { EventType } from "constants/ActionConstants";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { TriggerPropertiesMap } from "utils/WidgetFactory";
class CheckboxWidget extends BaseWidget<CheckboxWidgetProps, WidgetState> {
static getPropertyValidationMap(): WidgetPropertyValidationType {
@ -16,6 +17,12 @@ class CheckboxWidget extends BaseWidget<CheckboxWidgetProps, WidgetState> {
};
}
static getTriggerPropertyMap(): TriggerPropertiesMap {
return {
onCheckChange: true,
};
}
getPageView() {
return (
<CheckboxComponent
@ -32,7 +39,14 @@ class CheckboxWidget extends BaseWidget<CheckboxWidgetProps, WidgetState> {
onCheckChange = (isChecked: boolean) => {
this.updateWidgetProperty("isChecked", isChecked);
super.executeAction(this.props.onCheckChange);
if (this.props.onCheckChange) {
super.executeAction({
dynamicString: this.props.onCheckChange,
event: {
type: EventType.ON_CHECK_CHANGE,
},
});
}
};
getWidgetType(): WidgetType {
@ -45,7 +59,7 @@ export interface CheckboxWidgetProps extends WidgetProps {
defaultCheckedState: boolean;
isChecked?: boolean;
isDisabled?: boolean;
onCheckChange?: ActionPayload[];
onCheckChange?: string;
}
export default CheckboxWidget;

View File

@ -1,10 +1,11 @@
import React from "react";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import { ActionPayload } from "constants/ActionConstants";
import { EventType } from "constants/ActionConstants";
import DatePickerComponent from "components/designSystems/blueprint/DatePickerComponent";
import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
import { TriggerPropertiesMap } from "utils/WidgetFactory";
class DatePickerWidget extends BaseWidget<DatePickerWidgetProps, WidgetState> {
static getPropertyValidationMap(): WidgetPropertyValidationType {
@ -20,6 +21,11 @@ class DatePickerWidget extends BaseWidget<DatePickerWidgetProps, WidgetState> {
minDate: VALIDATION_TYPES.DATE,
};
}
static getTriggerPropertyMap(): TriggerPropertiesMap {
return {
onDateSelected: true,
};
}
getPageView() {
return (
<DatePickerComponent
@ -39,7 +45,14 @@ class DatePickerWidget extends BaseWidget<DatePickerWidgetProps, WidgetState> {
onDateSelected = (selectedDate: Date) => {
this.updateWidgetProperty("selectedDate", selectedDate);
super.executeAction(this.props.onDateSelected);
if (this.props.onDateSelected) {
super.executeAction({
dynamicString: this.props.onDateSelected,
event: {
type: EventType.ON_DATE_SELECTED,
},
});
}
};
getWidgetType(): WidgetType {
@ -57,8 +70,8 @@ export interface DatePickerWidgetProps extends WidgetProps {
dateFormat: string;
label: string;
datePickerType: DatePickerType;
onDateSelected: ActionPayload[];
onDateRangeSelected: ActionPayload[];
onDateSelected?: string;
onDateRangeSelected?: string;
maxDate: Date;
minDate: Date;
}

View File

@ -1,11 +1,12 @@
import React from "react";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import { ActionPayload } from "constants/ActionConstants";
import { EventType } from "constants/ActionConstants";
import DropDownComponent from "components/designSystems/blueprint/DropdownComponent";
import _ from "lodash";
import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
import { TriggerPropertiesMap } from "utils/WidgetFactory";
export interface DropDownDerivedProps {
selectedOption?: DropdownOption;
@ -30,9 +31,8 @@ class DropdownWidget extends BaseWidget<DropdownWidgetProps, WidgetState> {
: undefined
}}`,
selectedOptionArr: `{{
const options = this.options || [];
this.selectionType === "MULTI_SELECT"
? options.filter((opt, index) =>
? this.options.filter((opt, index) =>
_.includes(this.selectedIndexArr, index),
)
: undefined
@ -40,6 +40,12 @@ class DropdownWidget extends BaseWidget<DropdownWidgetProps, WidgetState> {
};
}
static getTriggerPropertyMap(): TriggerPropertiesMap {
return {
onOptionChange: true,
};
}
componentDidUpdate(prevProps: DropdownWidgetProps) {
super.componentDidUpdate(prevProps);
if (
@ -103,7 +109,14 @@ class DropdownWidget extends BaseWidget<DropdownWidgetProps, WidgetState> {
this.updateWidgetMetaProperty("selectedIndexArr", selectedIndexArr);
}
}
super.executeAction(this.props.onOptionChange);
if (this.props.onOptionChange) {
super.executeAction({
dynamicString: this.props.onOptionChange,
event: {
type: EventType.ON_OPTION_CHANGE,
},
});
}
};
onOptionRemoved = (removedIndex: number) => {
@ -113,7 +126,14 @@ class DropdownWidget extends BaseWidget<DropdownWidgetProps, WidgetState> {
})
: [];
this.updateWidgetMetaProperty("selectedIndexArr", updateIndexArr);
super.executeAction(this.props.onOptionChange);
if (this.props.onOptionChange) {
super.executeAction({
dynamicString: this.props.onOptionChange,
event: {
type: EventType.ON_OPTION_CHANGE,
},
});
}
};
getWidgetType(): WidgetType {
@ -125,6 +145,9 @@ export type SelectionType = "SINGLE_SELECT" | "MULTI_SELECT";
export interface DropdownOption {
label: string;
value: string;
id?: string;
onSelect?: (option: DropdownOption) => void;
children?: DropdownOption[];
}
export interface DropdownWidgetProps extends WidgetProps {
@ -134,7 +157,7 @@ export interface DropdownWidgetProps extends WidgetProps {
selectedIndexArr?: number[];
selectionType: SelectionType;
options?: DropdownOption[];
onOptionChange?: ActionPayload[];
onOptionChange?: string;
}
export default DropdownWidget;

View File

@ -2,11 +2,13 @@ import React from "react";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import InputComponent from "components/designSystems/blueprint/InputComponent";
import { ActionPayload } from "constants/ActionConstants";
import { EventType } from "constants/ActionConstants";
import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
import { TriggerPropertiesMap } from "utils/WidgetFactory";
class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
regex = new RegExp("");
static getPropertyValidationMap(): WidgetPropertyValidationType {
return {
inputType: VALIDATION_TYPES.TEXT,
@ -25,7 +27,11 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
isAutoFocusEnabled: VALIDATION_TYPES.BOOLEAN,
};
}
regex = new RegExp("");
static getTriggerPropertyMap(): TriggerPropertiesMap {
return {
onTextChanged: true,
};
}
componentDidMount() {
super.componentDidMount();
@ -51,7 +57,14 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
onValueChange = (value: string) => {
this.updateWidgetProperty("text", value);
super.executeAction(this.props.onTextChanged);
if (this.props.onTextChanged) {
super.executeAction({
dynamicString: this.props.onTextChanged,
event: {
type: EventType.ON_TEXT_CHANGE,
},
});
}
};
getPageView() {
@ -115,7 +128,7 @@ export interface InputWidgetProps extends WidgetProps {
maxChars?: number;
minNum?: number;
maxNum?: number;
onTextChanged?: ActionPayload[];
onTextChanged?: string;
label: string;
inputValidators: InputValidator[];
focusIndex?: number;

View File

@ -2,9 +2,10 @@ import React from "react";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import RadioGroupComponent from "components/designSystems/blueprint/RadioGroupComponent";
import { ActionPayload } from "constants/ActionConstants";
import { EventType } from "constants/ActionConstants";
import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
import { TriggerPropertiesMap } from "utils/WidgetFactory";
class RadioGroupWidget extends BaseWidget<RadioGroupWidgetProps, WidgetState> {
static getPropertyValidationMap(): WidgetPropertyValidationType {
@ -20,6 +21,11 @@ class RadioGroupWidget extends BaseWidget<RadioGroupWidgetProps, WidgetState> {
"{{_.find(this.options, { value: this.selectedOptionValue })}}",
};
}
static getTriggerPropertyMap(): TriggerPropertiesMap {
return {
onSelectionChange: true,
};
}
getPageView() {
return (
<RadioGroupComponent
@ -36,7 +42,14 @@ class RadioGroupWidget extends BaseWidget<RadioGroupWidgetProps, WidgetState> {
onRadioSelectionChange = (updatedValue: string) => {
this.updateWidgetProperty("selectedOptionValue", updatedValue);
super.executeAction(this.props.onSelectionChange);
if (this.props.onSelectionChange) {
super.executeAction({
dynamicString: this.props.onSelectionChange,
event: {
type: EventType.ON_OPTION_CHANGE,
},
});
}
};
getWidgetType(): WidgetType {
@ -54,7 +67,7 @@ export interface RadioGroupWidgetProps extends WidgetProps {
label: string;
options: RadioOption[];
selectedOptionValue: string;
onSelectionChange?: ActionPayload[];
onSelectionChange: string;
}
export default RadioGroupWidget;

View File

@ -1,7 +1,7 @@
import React from "react";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import { ActionPayload, TableAction } from "constants/ActionConstants";
import { EventType } from "constants/ActionConstants";
import { forIn } from "lodash";
import TableComponent from "components/designSystems/syncfusion/TableComponent";
@ -10,6 +10,7 @@ import { WidgetPropertyValidationType } from "utils/ValidationFactory";
import { ColumnModel } from "@syncfusion/ej2-grids";
import { ColumnDirTypecast } from "@syncfusion/ej2-react-grids";
import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl";
import { TriggerPropertiesMap } from "utils/WidgetFactory";
function constructColumns(data: object[]): ColumnModel[] | ColumnDirTypecast[] {
const cols: ColumnModel[] | ColumnDirTypecast[] = [];
@ -42,6 +43,14 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
};
}
static getTriggerPropertyMap(): TriggerPropertiesMap {
return {
onRowSelected: true,
onPageChange: true,
columnActions: true,
};
}
getPageView() {
const { tableData } = this.props;
const columns = constructColumns(tableData);
@ -67,29 +76,11 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
}}
columnActions={this.props.columnActions}
onCommandClick={this.onCommandClick}
onRowClick={(rowData: object, index: number) => {
const { onRowSelected } = this.props;
this.updateWidgetProperty("selectedRowIndex", index);
super.executeAction(onRowSelected);
}}
onRowClick={this.handleRowClick}
serverSidePaginationEnabled={serverSidePaginationEnabled}
pageNo={pageNo}
nextPageClick={() => {
let pageNo = this.props.pageNo || 1;
pageNo = pageNo + 1;
super.updateWidgetMetaProperty("pageNo", pageNo);
super.executeAction(this.props.onPageChange, "NEXT");
}}
prevPageClick={() => {
let pageNo = this.props.pageNo || 1;
pageNo = pageNo - 1;
if (pageNo >= 1) {
super.updateWidgetMetaProperty("pageNo", pageNo);
super.executeAction(this.props.onPageChange, "PREV");
}
}}
nextPageClick={this.handleNextPageClick}
prevPageClick={this.handlePrevPageClick}
updatePageNo={(pageNo: number) => {
super.updateWidgetMetaProperty("pageNo", pageNo);
}}
@ -100,8 +91,56 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
);
}
onCommandClick = (actions: ActionPayload[]) => {
super.executeAction(actions);
onCommandClick = (action: string) => {
super.executeAction({
dynamicString: action,
event: {
type: EventType.ON_CLICK,
},
});
};
handleRowClick = (rowData: object, index: number) => {
const { onRowSelected } = this.props;
super.updateWidgetProperty("selectedRow", index);
if (onRowSelected) {
super.executeAction({
dynamicString: onRowSelected,
event: {
type: EventType.ON_ROW_SELECTED,
},
});
}
};
handleNextPageClick = () => {
let pageNo = this.props.pageNo || 1;
pageNo = pageNo + 1;
super.updateWidgetMetaProperty("pageNo", pageNo);
if (this.props.onPageChange) {
super.executeAction({
dynamicString: this.props.onPageChange,
event: {
type: EventType.ON_NEXT_PAGE,
},
});
}
};
handlePrevPageClick = () => {
let pageNo = this.props.pageNo || 1;
pageNo = pageNo - 1;
if (pageNo >= 1) {
super.updateWidgetMetaProperty("pageNo", pageNo);
if (this.props.onPageChange) {
super.executeAction({
dynamicString: this.props.onPageChange,
event: {
type: EventType.ON_PREV_PAGE,
},
});
}
}
};
getWidgetType(): WidgetType {
@ -119,9 +158,8 @@ export interface TableWidgetProps extends WidgetProps {
prevPageKey?: string;
label: string;
tableData: object[];
recordActions?: TableAction[];
onPageChange?: ActionPayload[];
onRowSelected?: ActionPayload[];
onPageChange?: string;
onRowSelected?: string;
selectedRowIndex?: number;
columnActions?: ColumnAction[];
serverSidePaginationEnabled?: boolean;

View File

@ -7,7 +7,7 @@ export const mockExecute = jest.fn().mockImplementation((src, data) => {
});
finalSource = finalSource.substring(0, finalSource.length - 2) + ";";
finalSource += src;
return eval(finalSource);
return { result: eval(finalSource), triggers: [] };
});
export const mockRegisterLibrary = jest.fn();

1
app/client/typings/json-fn/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module "json-fn";

View File

@ -931,7 +931,7 @@
core-js-pure "^3.0.0"
regenerator-runtime "^0.13.2"
"@babel/runtime@7.8.4", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6":
"@babel/runtime@7.8.4", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6":
version "7.8.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308"
integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==
@ -9303,6 +9303,11 @@ jsesc@~0.5.0:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
json-fn@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/json-fn/-/json-fn-1.1.1.tgz#4293c9198a482d6697d334a6e32cd0d221121e80"
integrity sha1-QpPJGYpILWaX0zSm4yzQ0iESHoA=
json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
@ -13006,6 +13011,16 @@ react-textarea-autosize@^7.1.0:
"@babel/runtime" "^7.1.2"
prop-types "^15.6.0"
react-toastify@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-5.5.0.tgz#f55de44f6b5e3ce3b13b69e5bb4427f2c9404822"
integrity sha512-jsVme7jALIFGRyQsri/g4YTsRuaaGI70T6/ikjwZMB4mwTZaCWqj5NqxhGrRStKlJc5npXKKvKeqTiRGQl78LQ==
dependencies:
"@babel/runtime" "^7.4.2"
classnames "^2.2.6"
prop-types "^15.7.2"
react-transition-group "^4"
react-transition-group@^2.2.1, react-transition-group@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
@ -13016,7 +13031,7 @@ react-transition-group@^2.2.1, react-transition-group@^2.9.0:
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
react-transition-group@^4.3.0:
react-transition-group@^4, react-transition-group@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.3.0.tgz#fea832e386cf8796c58b61874a3319704f5ce683"
integrity sha512-1qRV1ZuVSdxPlPf4O8t7inxUGpdyO5zG9IoNfJxSO0ImU2A1YWkEQvFPuIPZmMLkg5hYs7vv5mMOyfgSkvAwvw==