diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ThemingTests/Theme_FormWidget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ThemingTests/Theme_FormWidget_spec.js index efb331a6db..7e1efdd686 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ThemingTests/Theme_FormWidget_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ThemingTests/Theme_FormWidget_spec.js @@ -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", diff --git a/app/client/src/constants/PropertyControlConstants.tsx b/app/client/src/constants/PropertyControlConstants.tsx index 97f9a0341a..1a7b8c42d9 100644 --- a/app/client/src/constants/PropertyControlConstants.tsx +++ b/app/client/src/constants/PropertyControlConstants.tsx @@ -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 | undefined; - hidden?: (props: any, propertyPath: string) => boolean; + hidden?: ( + props: any, + propertyPath: string, + widgetParentProps?: WidgetProps, + ) => boolean; invisible?: boolean; isBindProperty: boolean; isTriggerProperty: boolean; diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx index 387abbc039..94dbe98629 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx @@ -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; diff --git a/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx b/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx index f377193d2e..045d09e676 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertySection.tsx @@ -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; } } diff --git a/app/client/src/sagas/ActionExecution/ActionExecutionSagas.ts b/app/client/src/sagas/ActionExecution/ActionExecutionSagas.ts index 9d3991d2d7..40e30754a4 100644 --- a/app/client/src/sagas/ActionExecution/ActionExecutionSagas.ts +++ b/app/client/src/sagas/ActionExecution/ActionExecutionSagas.ts @@ -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) { diff --git a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts index 4a286f06c0..46d6a3b57d 100644 --- a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts +++ b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts @@ -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, "success", true), + params, + ]; } function* runActionShortcutSaga() { diff --git a/app/client/src/sagas/selectors.tsx b/app/client/src/sagas/selectors.tsx index 83328068fd..bab51ccb77 100644 --- a/app/client/src/sagas/selectors.tsx +++ b/app/client/src/sagas/selectors.tsx @@ -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; + }, + ); +}; diff --git a/app/client/src/widgets/ButtonWidget/index.ts b/app/client/src/widgets/ButtonWidget/index.ts index 59aed7fc31..bd7d0b78d0 100644 --- a/app/client/src/widgets/ButtonWidget/index.ts +++ b/app/client/src/widgets/ButtonWidget/index.ts @@ -23,6 +23,8 @@ export const CONFIG = { isDisabled: false, isVisible: true, isDefaultClickDisabled: true, + disabledWhenInvalid: false, + resetFormOnClick: false, recaptchaType: RecaptchaTypes.V3, version: 1, }, diff --git a/app/client/src/widgets/ButtonWidget/widget/index.tsx b/app/client/src/widgets/ButtonWidget/widget/index.tsx index 57a93a6b0b..313912826f 100644 --- a/app/client/src/widgets/ButtonWidget/widget/index.tsx +++ b/app/client/src/widgets/ButtonWidget/widget/index.tsx @@ -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 { onButtonClickBound: (event: React.MouseEvent) => void; @@ -121,6 +125,39 @@ class ButtonWidget extends BaseWidget { }, ], }, + // 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 { callback: this.handleActionComplete, }, }); + } else if (this.props.resetFormOnClick && this.props.onReset) { + this.props.onReset(); } } @@ -341,13 +380,22 @@ class ButtonWidget extends BaseWidget { } }; - 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 ( { 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 { diff --git a/app/client/src/widgets/FormButtonWidget/index.ts b/app/client/src/widgets/FormButtonWidget/index.ts index 49c5724551..85d3ad8947 100644 --- a/app/client/src/widgets/FormButtonWidget/index.ts +++ b/app/client/src/widgets/FormButtonWidget/index.ts @@ -7,6 +7,8 @@ export const CONFIG = { name: "FormButton", iconSVG: IconSVG, hideCard: true, + isDeprecated: true, + replacement: "BUTTON_WIDGET", needsMeta: true, defaults: { rows: 4, diff --git a/app/client/src/widgets/FormButtonWidget/widget/index.tsx b/app/client/src/widgets/FormButtonWidget/widget/index.tsx index 26cfb886f9..0d4c9cc20e 100644 --- a/app/client/src/widgets/FormButtonWidget/widget/index.tsx +++ b/app/client/src/widgets/FormButtonWidget/widget/index.tsx @@ -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", diff --git a/app/client/src/widgets/FormWidget/index.ts b/app/client/src/widgets/FormWidget/index.ts index dec3702900..97efd2c97b 100644 --- a/app/client/src/widgets/FormWidget/index.ts +++ b/app/client/src/widgets/FormWidget/index.ts @@ -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,