Skip to content

Commit f45a155

Browse files
authored
fix: depercate form button widget (#14510)
1 parent 6ad308e commit f45a155

File tree

12 files changed

+157
-41
lines changed

12 files changed

+157
-41
lines changed

app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ThemingTests/Theme_FormWidget_spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ describe("Theme validation usecases", function() {
8181
.then(($childElem) => {
8282
cy.get($childElem).click({ force: true });
8383
cy.get(
84-
".t--draggable-formbuttonwidget button :contains('Submit')",
84+
".t--draggable-buttonwidget button :contains('Submit')",
8585
).should(
8686
"have.css",
8787
"font-family",

app/client/src/constants/PropertyControlConstants.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
77
import { CodeEditorExpected } from "components/editorComponents/CodeEditor";
88
import { UpdateWidgetPropertyPayload } from "actions/controlActions";
99
import { AppTheme } from "entities/AppTheming";
10+
import { WidgetProps } from "widgets/BaseWidget";
1011

1112
const ControlTypes = getPropertyControlTypes();
1213
export type ControlType = typeof ControlTypes[keyof typeof ControlTypes];
@@ -15,7 +16,11 @@ export type PropertyPaneSectionConfig = {
1516
sectionName: string;
1617
id?: string;
1718
children: PropertyPaneConfig[];
18-
hidden?: (props: any, propertyPath: string) => boolean;
19+
hidden?: (
20+
props: any,
21+
propertyPath: string,
22+
widgetParentProps?: WidgetProps,
23+
) => boolean;
1924
isDefaultOpen?: boolean;
2025
propertySectionPath?: string;
2126
};
@@ -61,7 +66,11 @@ export type PropertyPaneControlConfig = {
6166
propertyName: string,
6267
propertyValue: any,
6368
) => Array<PropertyHookUpdates> | undefined;
64-
hidden?: (props: any, propertyPath: string) => boolean;
69+
hidden?: (
70+
props: any,
71+
propertyPath: string,
72+
widgetParentProps?: WidgetProps,
73+
) => boolean;
6574
invisible?: boolean;
6675
isBindProperty: boolean;
6776
isTriggerProperty: boolean;

app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { TooltipComponent } from "design-system";
4949
import { ReactComponent as ResetIcon } from "assets/icons/control/undo_2.svg";
5050
import { AppTheme } from "entities/AppTheming";
5151
import { JS_TOGGLE_DISABLED_MESSAGE } from "@appsmith/constants/messages";
52+
import { getWidgetParent } from "sagas/selectors";
5253

5354
type Props = PropertyPaneControlConfig & {
5455
panel: IPanelProps;
@@ -71,6 +72,13 @@ const PropertyControl = memo((props: Props) => {
7172
isEqual,
7273
);
7374

75+
/**
76+
* get actual parent of widget
77+
* for button inside form, button's parent is form
78+
* for button on canvas, parent is main container
79+
*/
80+
const parentWidget = useSelector(getWidgetParent(widgetProperties.widgetId));
81+
7482
const enhancementSelector = getWidgetEnhancementSelector(
7583
widgetProperties.widgetId,
7684
);
@@ -410,7 +418,8 @@ const PropertyControl = memo((props: Props) => {
410418

411419
// Do not render the control if it needs to be hidden
412420
if (
413-
(props.hidden && props.hidden(widgetProperties, props.propertyName)) ||
421+
(props.hidden &&
422+
props.hidden(widgetProperties, props.propertyName, parentWidget)) ||
414423
props.invisible
415424
) {
416425
return null;

app/client/src/pages/Editor/PropertyPane/PropertySection.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { Collapse } from "@blueprintjs/core";
1111
import { useSelector } from "react-redux";
1212
import { getWidgetPropsForPropertyPane } from "selectors/propertyPaneSelectors";
1313
import styled from "constants/DefaultTheme";
14+
import { getWidgetParent } from "sagas/selectors";
15+
import { WidgetProps } from "widgets/BaseWidget";
1416

1517
const SectionWrapper = styled.div`
1618
position: relative;
@@ -53,7 +55,11 @@ type PropertySectionProps = {
5355
id: string;
5456
name: string;
5557
children?: ReactNode;
56-
hidden?: (props: any, propertyPath: string) => boolean;
58+
hidden?: (
59+
props: any,
60+
propertyPath: string,
61+
widgetParentProps?: WidgetProps,
62+
) => boolean;
5763
isDefaultOpen?: boolean;
5864
propertyPath?: string;
5965
};
@@ -69,8 +75,15 @@ export const PropertySection = memo((props: PropertySectionProps) => {
6975
const { isDefaultOpen = true } = props;
7076
const [isOpen, open] = useState(!!isDefaultOpen);
7177
const widgetProps: any = useSelector(getWidgetPropsForPropertyPane);
78+
/**
79+
* get actual parent of widget
80+
* for button inside form, button's parent is form
81+
* for button on canvas, parent is main container
82+
*/
83+
const parentWidget = useSelector(getWidgetParent(widgetProps.widgetId));
84+
7285
if (props.hidden) {
73-
if (props.hidden(widgetProps, props.propertyPath || "")) {
86+
if (props.hidden(widgetProps, props.propertyPath || "", parentWidget)) {
7487
return null;
7588
}
7689
}

app/client/src/sagas/ActionExecution/ActionExecutionSagas.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
} from "sagas/ActionExecution/GetCurrentLocationSaga";
5050
import { requestModalConfirmationSaga } from "sagas/UtilSagas";
5151
import { ModalType } from "reducers/uiReducers/modalActionReducer";
52+
import { get, set, size } from "lodash";
5253

5354
export type TriggerMeta = {
5455
source?: TriggerSource;
@@ -67,7 +68,7 @@ export function* executeActionTriggers(
6768
triggerMeta: TriggerMeta,
6869
): any {
6970
// when called via a promise, a trigger can return some value to be used in .then
70-
let response: unknown[] = [];
71+
let response: unknown[] = [{ success: true }];
7172
switch (trigger.type) {
7273
case ActionTriggerType.RUN_PLUGIN_ACTION:
7374
response = yield call(
@@ -117,6 +118,8 @@ export function* executeActionTriggers(
117118
eventType,
118119
triggerMeta,
119120
);
121+
// response return only one object into array
122+
set(response, "0.success", true);
120123
break;
121124

122125
case ActionTriggerType.WATCH_CURRENT_LOCATION:
@@ -126,10 +129,14 @@ export function* executeActionTriggers(
126129
eventType,
127130
triggerMeta,
128131
);
132+
// response return only one object into array
133+
set(response, "0.success", true);
129134
break;
130135

131136
case ActionTriggerType.STOP_WATCHING_CURRENT_LOCATION:
132137
response = yield call(stopWatchCurrentLocation, eventType, triggerMeta);
138+
// response return only one object into array
139+
set(response, "0.success", true);
133140
break;
134141
case ActionTriggerType.CONFIRMATION_MODAL:
135142
const payloadInfo = {
@@ -149,7 +156,7 @@ export function* executeActionTriggers(
149156
return response;
150157
}
151158

152-
export function* executeAppAction(payload: ExecuteTriggerPayload) {
159+
export function* executeAppAction(payload: ExecuteTriggerPayload): any {
153160
const {
154161
callbackData,
155162
dynamicString,
@@ -163,7 +170,7 @@ export function* executeAppAction(payload: ExecuteTriggerPayload) {
163170
throw new Error("Executing undefined action");
164171
}
165172

166-
yield call(
173+
return yield call(
167174
evaluateAndExecuteDynamicTrigger,
168175
dynamicString,
169176
type,
@@ -181,9 +188,16 @@ function* initiateActionTriggerExecution(
181188
// it will be created again while execution
182189
AppsmithConsole.deleteError(`${source?.id}-${triggerPropertyName}`);
183190
try {
184-
yield call(executeAppAction, action.payload);
191+
const res: unknown[] = yield call(executeAppAction, action.payload);
185192
if (event.callback) {
186-
event.callback({ success: true });
193+
/**
194+
* result.success flag added to fire notification after successfully trigger
195+
* size of triggers checked for dependent action trigger i.e call success message after getting current location
196+
*/
197+
const success = !!(
198+
get(res, "result.success") || size(get(res, "triggers"))
199+
);
200+
event.callback({ success });
187201
}
188202
} catch (e) {
189203
if (e instanceof UncaughtPromiseError || e instanceof TriggerFailureError) {

app/client/src/sagas/ActionExecution/PluginActionSaga.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,10 @@ export default function* executePluginActionTriggerSaga(
374374
callbackData: [payload.body, params],
375375
...triggerMeta,
376376
});
377+
throw new PluginTriggerFailureError(
378+
createMessage(ERROR_ACTION_EXECUTE_FAIL, action.name),
379+
[payload.body, params],
380+
);
377381
} else {
378382
throw new PluginTriggerFailureError(
379383
createMessage(ERROR_PLUGIN_ACTION_EXECUTE, action.name),
@@ -402,9 +406,14 @@ export default function* executePluginActionTriggerSaga(
402406
callbackData: [payload.body, params],
403407
...triggerMeta,
404408
});
409+
return [{ success: true }];
405410
}
406411
}
407-
return [payload.body, params];
412+
// added success flag for successfull api execution and handle callback
413+
return [
414+
set((payload.body || {}) as Record<string, unknown>, "success", true),
415+
params,
416+
];
408417
}
409418

410419
function* runActionShortcutSaga() {

app/client/src/sagas/selectors.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import {
66
} from "reducers/entityReducers/canvasWidgetsReducer";
77
import { WidgetProps } from "widgets/BaseWidget";
88
import _ from "lodash";
9-
import { WidgetType } from "constants/WidgetConstants";
9+
import {
10+
WidgetType,
11+
MAIN_CONTAINER_WIDGET_ID,
12+
} from "constants/WidgetConstants";
1013
import { ActionData } from "reducers/entityReducers/actionsReducer";
1114
import { Page } from "@appsmith/constants/ReduxActionConstants";
1215
import { getActions, getPlugins } from "selectors/entitiesSelector";
@@ -175,3 +178,30 @@ export const getWidgetImmediateChildren = createSelector(
175178
return childrenIds;
176179
},
177180
);
181+
182+
/**
183+
* get actual parent of widget based on widgetId
184+
* for button inside form, button's parent is form
185+
* for button on canvas, parent is main container
186+
*/
187+
export const getWidgetParent = (widgetId: string) => {
188+
return createSelector(
189+
getWidgets,
190+
(canvasWidgets: CanvasWidgetsReduxState) => {
191+
let widget = canvasWidgets[widgetId];
192+
// While this widget has a parent
193+
while (widget?.parentId) {
194+
// Get parent widget props
195+
const parent = _.get(canvasWidgets, widget.parentId, undefined);
196+
// keep walking up the tree to find the parent untill parent exist or parent is the main container
197+
if (parent?.parentId && parent.parentId !== MAIN_CONTAINER_WIDGET_ID) {
198+
widget = canvasWidgets[widget.parentId];
199+
continue;
200+
} else {
201+
return parent;
202+
}
203+
}
204+
return;
205+
},
206+
);
207+
};

app/client/src/widgets/ButtonWidget/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export const CONFIG = {
2323
isDisabled: false,
2424
isVisible: true,
2525
isDefaultClickDisabled: true,
26+
disabledWhenInvalid: false,
27+
resetFormOnClick: false,
2628
recaptchaType: RecaptchaTypes.V3,
2729
version: 1,
2830
},

app/client/src/widgets/ButtonWidget/widget/index.tsx

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import React from "react";
22
import BaseWidget, { WidgetProps, WidgetState } from "widgets/BaseWidget";
33
import { WidgetType } from "constants/WidgetConstants";
44
import ButtonComponent, { ButtonType } from "../component";
5-
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
5+
import {
6+
EventType,
7+
ExecutionResult,
8+
} from "constants/AppsmithActionConstants/ActionConstants";
69
import { ValidationTypes } from "constants/WidgetValidation";
710
import { DerivedPropertiesMap } from "utils/WidgetFactory";
811
import { Alignment } from "@blueprintjs/core";
@@ -15,6 +18,7 @@ import {
1518
ButtonPlacementTypes,
1619
ButtonPlacement,
1720
} from "components/constants";
21+
import FormWidget from "widgets/FormWidget/widget";
1822

1923
class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
2024
onButtonClickBound: (event: React.MouseEvent<HTMLElement>) => void;
@@ -121,6 +125,39 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
121125
},
122126
],
123127
},
128+
// TODO: refactor widgetParentProps implementation when we address #10659
129+
{
130+
sectionName: "Form options",
131+
hidden: (
132+
props: ButtonWidgetProps,
133+
propertyPath: string,
134+
widgetParentProps?: WidgetProps,
135+
) => widgetParentProps?.type !== FormWidget.getWidgetType(),
136+
children: [
137+
{
138+
helpText:
139+
"Disabled if the form is invalid, if this widget exists directly within a Form widget.",
140+
propertyName: "disabledWhenInvalid",
141+
label: "Disabled Invalid Forms",
142+
controlType: "SWITCH",
143+
isJSConvertible: true,
144+
isBindProperty: true,
145+
isTriggerProperty: false,
146+
validation: { type: ValidationTypes.BOOLEAN },
147+
},
148+
{
149+
helpText:
150+
"Resets the fields of the form, on click, if this widget exists directly within a Form widget.",
151+
propertyName: "resetFormOnClick",
152+
label: "Reset Form on Success",
153+
controlType: "SWITCH",
154+
isJSConvertible: true,
155+
isBindProperty: true,
156+
isTriggerProperty: false,
157+
validation: { type: ValidationTypes.BOOLEAN },
158+
},
159+
],
160+
},
124161
{
125162
sectionName: "Events",
126163
children: [
@@ -321,6 +358,8 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
321358
callback: this.handleActionComplete,
322359
},
323360
});
361+
} else if (this.props.resetFormOnClick && this.props.onReset) {
362+
this.props.onReset();
324363
}
325364
}
326365

@@ -341,13 +380,22 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
341380
}
342381
};
343382

344-
handleActionComplete = () => {
383+
handleActionComplete = (result: ExecutionResult) => {
345384
this.setState({
346385
isLoading: false,
347386
});
387+
if (result.success) {
388+
if (this.props.resetFormOnClick && this.props.onReset)
389+
this.props.onReset();
390+
}
348391
};
349392

350393
getPageView() {
394+
const disabled =
395+
this.props.disabledWhenInvalid &&
396+
"isFormValid" in this.props &&
397+
!this.props.isFormValid;
398+
const isDisabled = this.props.isDisabled || disabled;
351399
return (
352400
<ButtonComponent
353401
borderRadius={this.props.borderRadius}
@@ -359,10 +407,10 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
359407
handleRecaptchaV2Loading={this.handleRecaptchaV2Loading}
360408
iconAlign={this.props.iconAlign}
361409
iconName={this.props.iconName}
362-
isDisabled={this.props.isDisabled}
410+
isDisabled={isDisabled}
363411
isLoading={this.props.isLoading || this.state.isLoading}
364412
key={this.props.widgetId}
365-
onClick={!this.props.isDisabled ? this.onButtonClickBound : undefined}
413+
onClick={isDisabled ? undefined : this.onButtonClickBound}
366414
placement={this.props.placement}
367415
recaptchaType={this.props.recaptchaType}
368416
text={this.props.text}
@@ -394,6 +442,8 @@ export interface ButtonWidgetProps extends WidgetProps {
394442
iconName?: IconName;
395443
iconAlign?: Alignment;
396444
placement?: ButtonPlacement;
445+
disabledWhenInvalid?: boolean;
446+
resetFormOnClick?: boolean;
397447
}
398448

399449
interface ButtonWidgetState extends WidgetState {

app/client/src/widgets/FormButtonWidget/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export const CONFIG = {
77
name: "FormButton",
88
iconSVG: IconSVG,
99
hideCard: true,
10+
isDeprecated: true,
11+
replacement: "BUTTON_WIDGET",
1012
needsMeta: true,
1113
defaults: {
1214
rows: 4,

0 commit comments

Comments
 (0)