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) => { .then(($childElem) => {
cy.get($childElem).click({ force: true }); cy.get($childElem).click({ force: true });
cy.get( cy.get(
".t--draggable-formbuttonwidget button :contains('Submit')", ".t--draggable-buttonwidget button :contains('Submit')",
).should( ).should(
"have.css", "have.css",
"font-family", "font-family",

View File

@ -7,6 +7,7 @@ import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
import { CodeEditorExpected } from "components/editorComponents/CodeEditor"; import { CodeEditorExpected } from "components/editorComponents/CodeEditor";
import { UpdateWidgetPropertyPayload } from "actions/controlActions"; import { UpdateWidgetPropertyPayload } from "actions/controlActions";
import { AppTheme } from "entities/AppTheming"; import { AppTheme } from "entities/AppTheming";
import { WidgetProps } from "widgets/BaseWidget";
const ControlTypes = getPropertyControlTypes(); const ControlTypes = getPropertyControlTypes();
export type ControlType = typeof ControlTypes[keyof typeof ControlTypes]; export type ControlType = typeof ControlTypes[keyof typeof ControlTypes];
@ -15,7 +16,11 @@ export type PropertyPaneSectionConfig = {
sectionName: string; sectionName: string;
id?: string; id?: string;
children: PropertyPaneConfig[]; children: PropertyPaneConfig[];
hidden?: (props: any, propertyPath: string) => boolean; hidden?: (
props: any,
propertyPath: string,
widgetParentProps?: WidgetProps,
) => boolean;
isDefaultOpen?: boolean; isDefaultOpen?: boolean;
propertySectionPath?: string; propertySectionPath?: string;
}; };
@ -61,7 +66,11 @@ export type PropertyPaneControlConfig = {
propertyName: string, propertyName: string,
propertyValue: any, propertyValue: any,
) => Array<PropertyHookUpdates> | undefined; ) => Array<PropertyHookUpdates> | undefined;
hidden?: (props: any, propertyPath: string) => boolean; hidden?: (
props: any,
propertyPath: string,
widgetParentProps?: WidgetProps,
) => boolean;
invisible?: boolean; invisible?: boolean;
isBindProperty: boolean; isBindProperty: boolean;
isTriggerProperty: 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 { ReactComponent as ResetIcon } from "assets/icons/control/undo_2.svg";
import { AppTheme } from "entities/AppTheming"; import { AppTheme } from "entities/AppTheming";
import { JS_TOGGLE_DISABLED_MESSAGE } from "@appsmith/constants/messages"; import { JS_TOGGLE_DISABLED_MESSAGE } from "@appsmith/constants/messages";
import { getWidgetParent } from "sagas/selectors";
type Props = PropertyPaneControlConfig & { type Props = PropertyPaneControlConfig & {
panel: IPanelProps; panel: IPanelProps;
@ -71,6 +72,13 @@ const PropertyControl = memo((props: Props) => {
isEqual, 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( const enhancementSelector = getWidgetEnhancementSelector(
widgetProperties.widgetId, widgetProperties.widgetId,
); );
@ -410,7 +418,8 @@ const PropertyControl = memo((props: Props) => {
// Do not render the control if it needs to be hidden // Do not render the control if it needs to be hidden
if ( if (
(props.hidden && props.hidden(widgetProperties, props.propertyName)) || (props.hidden &&
props.hidden(widgetProperties, props.propertyName, parentWidget)) ||
props.invisible props.invisible
) { ) {
return null; return null;

View File

@ -11,6 +11,8 @@ import { Collapse } from "@blueprintjs/core";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { getWidgetPropsForPropertyPane } from "selectors/propertyPaneSelectors"; import { getWidgetPropsForPropertyPane } from "selectors/propertyPaneSelectors";
import styled from "constants/DefaultTheme"; import styled from "constants/DefaultTheme";
import { getWidgetParent } from "sagas/selectors";
import { WidgetProps } from "widgets/BaseWidget";
const SectionWrapper = styled.div` const SectionWrapper = styled.div`
position: relative; position: relative;
@ -53,7 +55,11 @@ type PropertySectionProps = {
id: string; id: string;
name: string; name: string;
children?: ReactNode; children?: ReactNode;
hidden?: (props: any, propertyPath: string) => boolean; hidden?: (
props: any,
propertyPath: string,
widgetParentProps?: WidgetProps,
) => boolean;
isDefaultOpen?: boolean; isDefaultOpen?: boolean;
propertyPath?: string; propertyPath?: string;
}; };
@ -69,8 +75,15 @@ export const PropertySection = memo((props: PropertySectionProps) => {
const { isDefaultOpen = true } = props; const { isDefaultOpen = true } = props;
const [isOpen, open] = useState(!!isDefaultOpen); const [isOpen, open] = useState(!!isDefaultOpen);
const widgetProps: any = useSelector(getWidgetPropsForPropertyPane); 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) {
if (props.hidden(widgetProps, props.propertyPath || "")) { if (props.hidden(widgetProps, props.propertyPath || "", parentWidget)) {
return null; return null;
} }
} }

View File

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

View File

@ -374,6 +374,10 @@ export default function* executePluginActionTriggerSaga(
callbackData: [payload.body, params], callbackData: [payload.body, params],
...triggerMeta, ...triggerMeta,
}); });
throw new PluginTriggerFailureError(
createMessage(ERROR_ACTION_EXECUTE_FAIL, action.name),
[payload.body, params],
);
} else { } else {
throw new PluginTriggerFailureError( throw new PluginTriggerFailureError(
createMessage(ERROR_PLUGIN_ACTION_EXECUTE, action.name), createMessage(ERROR_PLUGIN_ACTION_EXECUTE, action.name),
@ -402,9 +406,14 @@ export default function* executePluginActionTriggerSaga(
callbackData: [payload.body, params], callbackData: [payload.body, params],
...triggerMeta, ...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() { function* runActionShortcutSaga() {

View File

@ -6,7 +6,10 @@ import {
} from "reducers/entityReducers/canvasWidgetsReducer"; } from "reducers/entityReducers/canvasWidgetsReducer";
import { WidgetProps } from "widgets/BaseWidget"; import { WidgetProps } from "widgets/BaseWidget";
import _ from "lodash"; import _ from "lodash";
import { WidgetType } from "constants/WidgetConstants"; import {
WidgetType,
MAIN_CONTAINER_WIDGET_ID,
} from "constants/WidgetConstants";
import { ActionData } from "reducers/entityReducers/actionsReducer"; import { ActionData } from "reducers/entityReducers/actionsReducer";
import { Page } from "@appsmith/constants/ReduxActionConstants"; import { Page } from "@appsmith/constants/ReduxActionConstants";
import { getActions, getPlugins } from "selectors/entitiesSelector"; import { getActions, getPlugins } from "selectors/entitiesSelector";
@ -175,3 +178,30 @@ export const getWidgetImmediateChildren = createSelector(
return childrenIds; 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, isDisabled: false,
isVisible: true, isVisible: true,
isDefaultClickDisabled: true, isDefaultClickDisabled: true,
disabledWhenInvalid: false,
resetFormOnClick: false,
recaptchaType: RecaptchaTypes.V3, recaptchaType: RecaptchaTypes.V3,
version: 1, version: 1,
}, },

View File

@ -2,7 +2,10 @@ import React from "react";
import BaseWidget, { WidgetProps, WidgetState } from "widgets/BaseWidget"; import BaseWidget, { WidgetProps, WidgetState } from "widgets/BaseWidget";
import { WidgetType } from "constants/WidgetConstants"; import { WidgetType } from "constants/WidgetConstants";
import ButtonComponent, { ButtonType } from "../component"; import ButtonComponent, { ButtonType } from "../component";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import {
EventType,
ExecutionResult,
} from "constants/AppsmithActionConstants/ActionConstants";
import { ValidationTypes } from "constants/WidgetValidation"; import { ValidationTypes } from "constants/WidgetValidation";
import { DerivedPropertiesMap } from "utils/WidgetFactory"; import { DerivedPropertiesMap } from "utils/WidgetFactory";
import { Alignment } from "@blueprintjs/core"; import { Alignment } from "@blueprintjs/core";
@ -15,6 +18,7 @@ import {
ButtonPlacementTypes, ButtonPlacementTypes,
ButtonPlacement, ButtonPlacement,
} from "components/constants"; } from "components/constants";
import FormWidget from "widgets/FormWidget/widget";
class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> { class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
onButtonClickBound: (event: React.MouseEvent<HTMLElement>) => void; 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", sectionName: "Events",
children: [ children: [
@ -321,6 +358,8 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
callback: this.handleActionComplete, 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({ this.setState({
isLoading: false, isLoading: false,
}); });
if (result.success) {
if (this.props.resetFormOnClick && this.props.onReset)
this.props.onReset();
}
}; };
getPageView() { getPageView() {
const disabled =
this.props.disabledWhenInvalid &&
"isFormValid" in this.props &&
!this.props.isFormValid;
const isDisabled = this.props.isDisabled || disabled;
return ( return (
<ButtonComponent <ButtonComponent
borderRadius={this.props.borderRadius} borderRadius={this.props.borderRadius}
@ -359,10 +407,10 @@ class ButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
handleRecaptchaV2Loading={this.handleRecaptchaV2Loading} handleRecaptchaV2Loading={this.handleRecaptchaV2Loading}
iconAlign={this.props.iconAlign} iconAlign={this.props.iconAlign}
iconName={this.props.iconName} iconName={this.props.iconName}
isDisabled={this.props.isDisabled} isDisabled={isDisabled}
isLoading={this.props.isLoading || this.state.isLoading} isLoading={this.props.isLoading || this.state.isLoading}
key={this.props.widgetId} key={this.props.widgetId}
onClick={!this.props.isDisabled ? this.onButtonClickBound : undefined} onClick={isDisabled ? undefined : this.onButtonClickBound}
placement={this.props.placement} placement={this.props.placement}
recaptchaType={this.props.recaptchaType} recaptchaType={this.props.recaptchaType}
text={this.props.text} text={this.props.text}
@ -394,6 +442,8 @@ export interface ButtonWidgetProps extends WidgetProps {
iconName?: IconName; iconName?: IconName;
iconAlign?: Alignment; iconAlign?: Alignment;
placement?: ButtonPlacement; placement?: ButtonPlacement;
disabledWhenInvalid?: boolean;
resetFormOnClick?: boolean;
} }
interface ButtonWidgetState extends WidgetState { interface ButtonWidgetState extends WidgetState {

View File

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

View File

@ -47,28 +47,6 @@ class FormButtonWidget extends ButtonWidget {
isTriggerProperty: false, isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT }, 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", propertyName: "isVisible",
label: "Visible", label: "Visible",

View File

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