Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,8 @@ export interface ButtonProps extends HeadlessButtonProps {
* @default medium
*/
size?: Omit<keyof typeof SIZES, "large">;
/** Indicates if the button should be disabled when the form is invalid */
disableOnInvalidForm?: boolean;
/** Indicates if the button should reset the form when clicked */
resetFormOnClick?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@ export function RecaptchaV2(props: RecaptchaV2Props) {
const {
isDisabled,
isLoading,
onPress: onClickProp,
onClick: onClickProp,
onRecaptchaSubmitError = noop,
onRecaptchaSubmitSuccess,
onReset,
recaptchaKey,
} = props;
const onClick = () => {
if (isDisabled) return onClickProp;
if (isDisabled) return () => onClickProp?.(onReset);

if (isLoading) return onClickProp;
if (isLoading) return () => onClickProp?.(onReset);

if (isInvalidKey) {
// Handle incorrent google recaptcha site key
Expand All @@ -43,7 +44,7 @@ export function RecaptchaV2(props: RecaptchaV2Props) {
.then((token: any) => {
if (token) {
if (typeof onRecaptchaSubmitSuccess === "function") {
onRecaptchaSubmitSuccess(token);
onRecaptchaSubmitSuccess(token, onReset);
}
} else {
// Handle incorrent google recaptcha site key
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@ export function RecaptchaV3(props: RecaptchaV3Props) {
};

const {
onPress: onClickProp,
onClick: onClickProp,
onRecaptchaSubmitError = noop,
onRecaptchaSubmitSuccess,
onReset,
recaptchaKey,
} = props;

const onClick: ButtonComponentProps["onPress"] = () => {
if (props.isDisabled) return onClickProp;
if (props.isDisabled) return () => onClickProp?.(onReset);

if (props.isLoading) return onClickProp;
if (props.isLoading) return () => onClickProp?.(onReset);

if (status === ScriptStatus.READY) {
// TODO: Fix this the next time the file is edited
Expand All @@ -42,7 +43,7 @@ export function RecaptchaV3(props: RecaptchaV3Props) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.then((token: any) => {
if (typeof onRecaptchaSubmitSuccess === "function") {
onRecaptchaSubmitSuccess(token);
onRecaptchaSubmitSuccess(token, onReset);
}
})
.catch(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
import React from "react";
import { Button, Tooltip } from "@appsmith/wds";
import type { ButtonProps } from "@appsmith/wds";

import { Container } from "./Container";
import { useRecaptcha } from "./useRecaptcha";
import type { UseRecaptchaProps } from "./useRecaptcha";
import { Button, Tooltip } from "@appsmith/wds";
import type { ButtonProps } from "@appsmith/wds";
import { useWDSZoneWidgetContext } from "../../WDSZoneWidget/widget/context";

export interface ButtonComponentProps extends ButtonProps {
text?: string;
tooltip?: string;
isVisible?: boolean;
isLoading: boolean;
isDisabled?: boolean;
onClick?: (onReset?: () => void) => void;
}

function ButtonComponent(props: ButtonComponentProps & UseRecaptchaProps) {
const { icon, text, tooltip, ...rest } = props;
const { isFormValid, onReset } = useWDSZoneWidgetContext();
const { onClick, recpatcha } = useRecaptcha({ ...props, onReset });

const { onClick, recpatcha } = useRecaptcha(props);
const isDisabled =
props.isDisabled || (props.disableOnInvalidForm && isFormValid === false);

return (
<Container>
<Tooltip tooltip={tooltip}>
<Button icon={icon} {...rest} onPress={onClick}>
<Button
icon={icon}
{...rest}
isDisabled={isDisabled}
onPress={() => onClick?.(onReset)}
>
{text}
</Button>
</Tooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ export interface UseRecaptchaProps {
recaptchaKey?: string;
recaptchaType?: RecaptchaType;
onRecaptchaSubmitError?: (error: string) => void;
onRecaptchaSubmitSuccess?: (token: string) => void;
onRecaptchaSubmitSuccess?: (token: string, onReset?: () => void) => void;
handleRecaptchaV2Loading?: (isLoading: boolean) => void;
}

export type RecaptchaProps = ButtonComponentProps & UseRecaptchaProps;
export type RecaptchaProps = ButtonComponentProps &
UseRecaptchaProps & {
onReset?: () => void;
onClick?: (onReset?: () => void) => void;
};

interface UseRecaptchaReturn {
// TODO: Fix this the next time the file is edited
Expand All @@ -21,10 +25,10 @@ interface UseRecaptchaReturn {
}

export const useRecaptcha = (props: RecaptchaProps): UseRecaptchaReturn => {
const { onPress: onClickProp, recaptchaKey } = props;
const { onClick: onClickProp, recaptchaKey } = props;

if (!recaptchaKey) {
return { onClick: onClickProp };
return { onClick: () => onClickProp?.(props?.onReset) };
}

if (props.recaptchaType === "V2") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const defaultsConfig = {
widgetName: "Button",
isDisabled: false,
isVisible: true,
disabledWhenInvalid: false,
disableOnInvalidForm: false,
resetFormOnClick: false,
recaptchaType: RecaptchaTypes.V3,
version: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,31 @@ export const propertyPaneContentConfig = [
},
],
},
{
sectionName: "Form settings",
children: [
{
helpText:
"Disabled if the form is invalid, if this widget exists directly within a Form widget.",
propertyName: "disableOnInvalidForm",
label: "Disable when form invalid",
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 },
},
],
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class WDSButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
return config.settersConfig;
}

onButtonClick = () => {
onButtonClick = (onReset?: () => void) => {
if (this.props.onClick) {
this.setState({ isLoading: true });

Expand All @@ -79,33 +79,35 @@ class WDSButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
dynamicString: this.props.onClick,
event: {
type: EventType.ON_CLICK,
callback: this.handleActionComplete,
callback: (result: ExecutionResult) =>
this.handleActionComplete(result, onReset),
},
});

return;
}

if (this.props.resetFormOnClick && this.props.onReset) {
this.props.onReset();
if (this.props.resetFormOnClick && onReset) {
onReset();

return;
}
};

hasOnClickAction = () => {
const { isDisabled, onClick, onReset, resetFormOnClick } = this.props;
const { isDisabled, onClick, resetFormOnClick } = this.props;

return Boolean((onClick || onReset || resetFormOnClick) && !isDisabled);
return Boolean((onClick || resetFormOnClick) && !isDisabled);
};

onRecaptchaSubmitSuccess = (token: string) => {
onRecaptchaSubmitSuccess = (token: string, onReset?: () => void) => {
this.props.updateWidgetMetaProperty("recaptchaToken", token, {
triggerPropertyName: "onClick",
dynamicString: this.props.onClick,
event: {
type: EventType.ON_CLICK,
callback: this.handleActionComplete,
callback: (result: ExecutionResult) =>
this.handleActionComplete(result, onReset),
},
});
};
Expand All @@ -124,49 +126,34 @@ class WDSButtonWidget extends BaseWidget<ButtonWidgetProps, ButtonWidgetState> {
}
};

handleActionComplete = (result: ExecutionResult) => {
handleActionComplete = (result: ExecutionResult, onReset?: () => void) => {
this.setState({
isLoading: false,
});

if (result.success) {
if (this.props.resetFormOnClick && this.props.onReset)
this.props.onReset();
if (this.props.resetFormOnClick && onReset) onReset();
}
};

getWidgetView() {
const isDisabled = (() => {
const { disabledWhenInvalid, isFormValid } = this.props;
const isDisabledWhenFormIsInvalid =
disabledWhenInvalid && "isFormValid" in this.props && !isFormValid;

return this.props.isDisabled || isDisabledWhenFormIsInvalid;
})();

const onPress = (() => {
if (this.hasOnClickAction()) {
return this.onButtonClick;
}

return undefined;
})();

return (
<ButtonComponent
color={this.props.buttonColor}
disableOnInvalidForm={this.props.disableOnInvalidForm}
excludeFromTabOrder={this.props.disableWidgetInteraction}
handleRecaptchaV2Loading={this.handleRecaptchaV2Loading}
icon={this.props.iconName}
iconPosition={this.props.iconAlign}
isDisabled={isDisabled}
isDisabled={this.props.isDisabled}
isLoading={this.props.isLoading || this.state.isLoading}
key={this.props.widgetId}
onPress={onPress}
onClick={this.onButtonClick}
onRecaptchaSubmitError={this.onRecaptchaSubmitError}
onRecaptchaSubmitSuccess={this.onRecaptchaSubmitSuccess}
recaptchaKey={this.props.googleRecaptchaKey}
recaptchaType={this.props.recaptchaType}
resetFormOnClick={this.props.resetFormOnClick}
text={this.props.text}
tooltip={this.props.tooltip}
variant={this.props.buttonVariant}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ export interface ButtonWidgetState extends WidgetState {

export interface ButtonWidgetProps
extends WidgetProps,
Omit<ButtonComponentProps, "type"> {
Omit<ButtonComponentProps, "type" | "onClick"> {
text?: string;
isVisible?: boolean;
isDisabled?: boolean;
resetFormOnClick?: boolean;
googleRecaptchaKey?: string;
recaptchaType?: RecaptchaType;
disabledWhenInvalid?: boolean;
onClick?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const defaultConfig: WidgetDefaultProps = {
version: 1,
widgetName: "Zone",
isVisible: true,
useAsForm: false,
blueprint: {
operations: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ export const propertyPaneContent = [
{
sectionName: "General",
children: [
{
propertyName: "useAsForm",
label: "Use as a form",
helpText: "Controls the visibility of the widget",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
Comment on lines +30 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect helpText for useAsForm property.

The helpText "Controls the visibility of the widget" is incorrect for the useAsForm property. It should describe the form functionality instead.

-        helpText: "Controls the visibility of the widget",
+        helpText: "Enables form functionality for this zone widget",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
propertyName: "useAsForm",
label: "Use as a form",
helpText: "Controls the visibility of the widget",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
{
propertyName: "useAsForm",
label: "Use as a form",
helpText: "Enables form functionality for this zone widget",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},

{
helpText: "Controls the visibility of the widget",
propertyName: "isVisible",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useSelector } from "react-redux";
import { getCanvasWidgets } from "ee/selectors/entitiesSelector";
import React, {
createContext,
useContext,
useMemo,
type ReactNode,
} from "react";
import type { WidgetProps } from "widgets/BaseWidget";
import { getDataTree } from "selectors/dataTreeSelectors";

interface WDSZoneWidgetContextType {
isFormValid: boolean;
onReset?: () => void;
}

const WDSZoneWidgetContext = createContext<
WDSZoneWidgetContextType | undefined
>(undefined);

export const useWDSZoneWidgetContext = () => {
const context = useContext(WDSZoneWidgetContext);

if (context === undefined) {
throw new Error(
"useWDSZoneWidgetContext must be used within a WDSZoneWidgetProvider",
);
}

return context;
};

export const WDSZoneWidgetContextProvider = (props: {
children: ReactNode;
widget: WidgetProps;
useAsForm?: boolean;
onReset?: () => void;
}) => {
const { onReset, useAsForm, widget } = props;
const canvasWidgets = useSelector(getCanvasWidgets);
const dataTree = useSelector(getDataTree);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@riodeuno Need your review here. To track the invalid state of children's, I am tapping into data tree. Hope that's okay.

const isFormValid = useMemo(() => {
if (!useAsForm) return true;

const children = widget.children as WidgetProps["children"];

return children.reduce((isValid: boolean, child: WidgetProps) => {
const widget = dataTree[canvasWidgets[child.widgetId].widgetName];

return "isValid" in widget ? widget.isValid && isValid : isValid;
}, true);
}, [widget, canvasWidgets, dataTree, useAsForm]);

return (
<WDSZoneWidgetContext.Provider value={{ isFormValid, onReset }}>
{props.children}
</WDSZoneWidgetContext.Provider>
);
};

export default WDSZoneWidgetContext;
Loading