fix: depercate form button widget (#14510)

This commit is contained in:
Bhavin K 2022-07-22 13:57:37 +05:30 committed by GitHub
parent 6ad308ec39
commit f45a15545d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 157 additions and 41 deletions

View File

@ -81,7 +81,7 @@ describe("Theme validation usecases", function() {
.then(($childElem) => {
cy.get($childElem).click({ force: true });
cy.get(
".t--draggable-formbuttonwidget button :contains('Submit')",
".t--draggable-buttonwidget button :contains('Submit')",
).should(
"have.css",
"font-family",

View File

@ -7,6 +7,7 @@ import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
import { CodeEditorExpected } from "components/editorComponents/CodeEditor";
import { UpdateWidgetPropertyPayload } from "actions/controlActions";
import { AppTheme } from "entities/AppTheming";
import { WidgetProps } from "widgets/BaseWidget";
const ControlTypes = getPropertyControlTypes();
export type ControlType = typeof ControlTypes[keyof typeof ControlTypes];
@ -15,7 +16,11 @@ export type PropertyPaneSectionConfig = {
sectionName: string;
id?: string;
children: PropertyPaneConfig[];
hidden?: (props: any, propertyPath: string) => boolean;
hidden?: (
props: any,
propertyPath: string,
widgetParentProps?: WidgetProps,
) => boolean;
isDefaultOpen?: boolean;
propertySectionPath?: string;
};
@ -61,7 +66,11 @@ export type PropertyPaneControlConfig = {
propertyName: string,
propertyValue: any,
) => Array<PropertyHookUpdates> | undefined;
hidden?: (props: any, propertyPath: string) => boolean;
hidden?: (
props: any,
propertyPath: string,
widgetParentProps?: WidgetProps,
) => boolean;
invisible?: boolean;
isBindProperty: boolean;
isTriggerProperty: boolean;

View File

@ -49,6 +49,7 @@ import { TooltipComponent } from "design-system";
import { ReactComponent as ResetIcon } from "assets/icons/control/undo_2.svg";
import { AppTheme } from "entities/AppTheming";
import { JS_TOGGLE_DISABLED_MESSAGE } from "@appsmith/constants/messages";
import { getWidgetParent } from "sagas/selectors";
type Props = PropertyPaneControlConfig & {
panel: IPanelProps;
@ -71,6 +72,13 @@ const PropertyControl = memo((props: Props) => {
isEqual,
);
/**
* get actual parent of widget
* for button inside form, button's parent is form
* for button on canvas, parent is main container
*/
const parentWidget = useSelector(getWidgetParent(widgetProperties.widgetId));
const enhancementSelector = getWidgetEnhancementSelector(
widgetProperties.widgetId,
);
@ -410,7 +418,8 @@ const PropertyControl = memo((props: Props) => {
// Do not render the control if it needs to be hidden
if (
(props.hidden && props.hidden(widgetProperties, props.propertyName)) ||
(props.hidden &&
props.hidden(widgetProperties, props.propertyName, parentWidget)) ||
props.invisible
) {
return null;

View File

@ -11,6 +11,8 @@ import { Collapse } from "@blueprintjs/core";
import { useSelector } from "react-redux";
import { getWidgetPropsForPropertyPane } from "selectors/propertyPaneSelectors";
import styled from "constants/DefaultTheme";
import { getWidgetParent } from "sagas/selectors";
import { WidgetProps } from "widgets/BaseWidget";
const SectionWrapper = styled.div`
position: relative;
@ -53,7 +55,11 @@ type PropertySectionProps = {
id: string;
name: string;
children?: ReactNode;
hidden?: (props: any, propertyPath: string) => boolean;
hidden?: (
props: any,
propertyPath: string,
widgetParentProps?: WidgetProps,
) => boolean;
isDefaultOpen?: boolean;
propertyPath?: string;
};
@ -69,8 +75,15 @@ export const PropertySection = memo((props: PropertySectionProps) => {
const { isDefaultOpen = true } = props;
const [isOpen, open] = useState(!!isDefaultOpen);
const widgetProps: any = useSelector(getWidgetPropsForPropertyPane);
/**
* get actual parent of widget
* for button inside form, button's parent is form
* for button on canvas, parent is main container
*/
const parentWidget = useSelector(getWidgetParent(widgetProps.widgetId));
if (props.hidden) {
if (props.hidden(widgetProps, props.propertyPath || "")) {
if (props.hidden(widgetProps, props.propertyPath || "", parentWidget)) {
return null;
}
}

View File

@ -49,6 +49,7 @@ import {
} from "sagas/ActionExecution/GetCurrentLocationSaga";
import { requestModalConfirmationSaga } from "sagas/UtilSagas";
import { ModalType } from "reducers/uiReducers/modalActionReducer";
import { get, set, size } from "lodash";
export type TriggerMeta = {
source?: TriggerSource;
@ -67,7 +68,7 @@ export function* executeActionTriggers(
triggerMeta: TriggerMeta,
): any {
// when called via a promise, a trigger can return some value to be used in .then
let response: unknown[] = [];
let response: unknown[] = [{ success: true }];
switch (trigger.type) {
case ActionTriggerType.RUN_PLUGIN_ACTION:
response = yield call(
@ -117,6 +118,8 @@ export function* executeActionTriggers(
eventType,
triggerMeta,
);
// response return only one object into array
set(response, "0.success", true);
break;
case ActionTriggerType.WATCH_CURRENT_LOCATION:
@ -126,10 +129,14 @@ export function* executeActionTriggers(
eventType,
triggerMeta,
);
// response return only one object into array
set(response, "0.success", true);
break;
case ActionTriggerType.STOP_WATCHING_CURRENT_LOCATION:
response = yield call(stopWatchCurrentLocation, eventType, triggerMeta);
// response return only one object into array
set(response, "0.success", true);
break;
case ActionTriggerType.CONFIRMATION_MODAL:
const payloadInfo = {
@ -149,7 +156,7 @@ export function* executeActionTriggers(
return response;
}
export function* executeAppAction(payload: ExecuteTriggerPayload) {
export function* executeAppAction(payload: ExecuteTriggerPayload): any {
const {
callbackData,
dynamicString,
@ -163,7 +170,7 @@ export function* executeAppAction(payload: ExecuteTriggerPayload) {
throw new Error("Executing undefined action");
}
yield call(
return yield call(
evaluateAndExecuteDynamicTrigger,
dynamicString,
type,
@ -181,9 +188,16 @@ function* initiateActionTriggerExecution(
// it will be created again while execution
AppsmithConsole.deleteError(`${source?.id}-${triggerPropertyName}`);
try {
yield call(executeAppAction, action.payload);
const res: unknown[] = yield call(executeAppAction, action.payload);
if (event.callback) {
event.callback({ success: true });
/**
* result.success flag added to fire notification after successfully trigger
* size of triggers checked for dependent action trigger i.e call success message after getting current location
*/
const success = !!(
get(res, "result.success") || size(get(res, "triggers"))
);
event.callback({ success });
}
} catch (e) {
if (e instanceof UncaughtPromiseError || e instanceof TriggerFailureError) {

View File

@ -374,6 +374,10 @@ export default function* executePluginActionTriggerSaga(
callbackData: [payload.body, params],
...triggerMeta,
});
throw new PluginTriggerFailureError(
createMessage(ERROR_ACTION_EXECUTE_FAIL, action.name),
[payload.body, params],
);
} else {
throw new PluginTriggerFailureError(
createMessage(ERROR_PLUGIN_ACTION_EXECUTE, action.name),
@ -402,9 +406,14 @@ export default function* executePluginActionTriggerSaga(
callbackData: [payload.body, params],
...triggerMeta,
});
return [{ success: true }];
}
}
return [payload.body, params];
// added success flag for successfull api execution and handle callback
return [
set((payload.body || {}) as Record<string, unknown>, "success", true),
params,
];
}
function* runActionShortcutSaga() {

View File

@ -6,7 +6,10 @@ import {
} from "reducers/entityReducers/canvasWidgetsReducer";
import { WidgetProps } from "widgets/BaseWidget";
import _ from "lodash";
import { WidgetType } from "constants/WidgetConstants";
import {
WidgetType,
MAIN_CONTAINER_WIDGET_ID,
} from "constants/WidgetConstants";
import { ActionData } from "reducers/entityReducers/actionsReducer";
import { Page } from "@appsmith/constants/ReduxActionConstants";
import { getActions, getPlugins } from "selectors/entitiesSelector";
@ -175,3 +178,30 @@ export const getWidgetImmediateChildren = createSelector(
return childrenIds;
},
);
/**
* get actual parent of widget based on widgetId
* for button inside form, button's parent is form
* for button on canvas, parent is main container
*/
export const getWidgetParent = (widgetId: string) => {
return createSelector(
getWidgets,
(canvasWidgets: CanvasWidgetsReduxState) => {
let widget = canvasWidgets[widgetId];
// While this widget has a parent
while (widget?.parentId) {
// Get parent widget props
const parent = _.get(canvasWidgets, widget.parentId, undefined);
// keep walking up the tree to find the parent untill parent exist or parent is the main container
if (parent?.parentId && parent.parentId !== MAIN_CONTAINER_WIDGET_ID) {
widget = canvasWidgets[widget.parentId];
continue;
} else {
return parent;
}
}
return;
},
);
};

View File

@ -23,6 +23,8 @@ export const CONFIG = {
isDisabled: false,
isVisible: true,
isDefaultClickDisabled: true,
disabledWhenInvalid: false,
resetFormOnClick: false,
recaptchaType: RecaptchaTypes.V3,
version: 1,
},

View File

@ -2,7 +2,10 @@ import React from "react";
import BaseWidget, { WidgetProps, WidgetState } from "widgets/BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import ButtonComponent, { ButtonType } from "../component";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import {
EventType,
ExecutionResult,
} from "constants/AppsmithActionConstants/ActionConstants";
import { ValidationTypes } from "constants/WidgetValidation";
import { DerivedPropertiesMap } from "utils/WidgetFactory";
import { Alignment } from "@blueprintjs/core";
@ -15,6 +18,7 @@ import {
ButtonPlacementTypes,
ButtonPlacement,
} from "components/constants";
import FormWidget from "widgets/FormWidget/widget";
class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
onButtonClickBound: (event: React.MouseEvent<HTMLElement>) => void;
@ -121,6 +125,39 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
},
],
},
// TODO: refactor widgetParentProps implementation when we address #10659
{
sectionName: "Form options",
hidden: (
props: ButtonWidgetProps,
propertyPath: string,
widgetParentProps?: WidgetProps,
) => widgetParentProps?.type !== FormWidget.getWidgetType(),
children: [
{
helpText:
"Disabled if the form is invalid, if this widget exists directly within a Form widget.",
propertyName: "disabledWhenInvalid",
label: "Disabled Invalid Forms",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
{
helpText:
"Resets the fields of the form, on click, if this widget exists directly within a Form widget.",
propertyName: "resetFormOnClick",
label: "Reset Form on Success",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
],
},
{
sectionName: "Events",
children: [
@ -321,6 +358,8 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
callback: this.handleActionComplete,
},
});
} else if (this.props.resetFormOnClick && this.props.onReset) {
this.props.onReset();
}
}
@ -341,13 +380,22 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
}
};
handleActionComplete = () => {
handleActionComplete = (result: ExecutionResult) => {
this.setState({
isLoading: false,
});
if (result.success) {
if (this.props.resetFormOnClick && this.props.onReset)
this.props.onReset();
}
};
getPageView() {
const disabled =
this.props.disabledWhenInvalid &&
"isFormValid" in this.props &&
!this.props.isFormValid;
const isDisabled = this.props.isDisabled || disabled;
return (
<ButtonComponent
borderRadius={this.props.borderRadius}
@ -359,10 +407,10 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
handleRecaptchaV2Loading={this.handleRecaptchaV2Loading}
iconAlign={this.props.iconAlign}
iconName={this.props.iconName}
isDisabled={this.props.isDisabled}
isDisabled={isDisabled}
isLoading={this.props.isLoading || this.state.isLoading}
key={this.props.widgetId}
onClick={!this.props.isDisabled ? this.onButtonClickBound : undefined}
onClick={isDisabled ? undefined : this.onButtonClickBound}
placement={this.props.placement}
recaptchaType={this.props.recaptchaType}
text={this.props.text}
@ -394,6 +442,8 @@ export interface ButtonWidgetProps extends WidgetProps {
iconName?: IconName;
iconAlign?: Alignment;
placement?: ButtonPlacement;
disabledWhenInvalid?: boolean;
resetFormOnClick?: boolean;
}
interface ButtonWidgetState extends WidgetState {

View File

@ -7,6 +7,8 @@ export const CONFIG = {
name: "FormButton",
iconSVG: IconSVG,
hideCard: true,
isDeprecated: true,
replacement: "BUTTON_WIDGET",
needsMeta: true,
defaults: {
rows: 4,

View File

@ -47,28 +47,6 @@ class FormButtonWidget extends ButtonWidget {
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
{
helpText:
"Disables the button when the parent form has a required widget that is not filled",
propertyName: "disabledWhenInvalid",
label: "Disabled Invalid Forms",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
{
helpText:
"Resets the fields within the parent form when the click action succeeds",
propertyName: "resetFormOnClick",
label: "Reset Form on Success",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
{
propertyName: "isVisible",
label: "Visible",

View File

@ -44,7 +44,7 @@ export const CONFIG = {
},
},
{
type: "FORM_BUTTON_WIDGET",
type: "BUTTON_WIDGET",
size: {
rows: 4,
cols: 16,
@ -63,7 +63,7 @@ export const CONFIG = {
},
},
{
type: "FORM_BUTTON_WIDGET",
type: "BUTTON_WIDGET",
size: {
rows: 4,
cols: 16,