Skip to content

Commit

Permalink
chore: Use zone as form (#38550)
Browse files Browse the repository at this point in the history
Fixes #38525 

/ok-to-test tags="@tag.Anvil"


https://github.com/user-attachments/assets/891ecf61-2850-46c4-acd3-b170196e5ab7



<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/12703524629>
> Commit: 64616e2
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=12703524629&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Anvil`
> Spec:
> <hr>Fri, 10 Jan 2025 05:20:43 UTC
<!-- end of auto-generated comment: Cypress test results  -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

- **New Features**
  - Added form validation controls for buttons
  - Introduced ability to disable buttons on invalid form
  - Added option to reset form on button click
  - New context provider for managing widget state in UI builder

- **Improvements**
  - Enhanced reCAPTCHA handling with reset functionality
  - Updated button click event management
  - Improved widget context management

- **Configuration Updates**
  - New form settings for button widgets
  - Added `useAsForm` configuration for zone widgets

These updates provide more granular control over form interactions and
button behaviors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
jsartisan authored Jan 10, 2025
1 parent 37354bc commit 65df8d4
Show file tree
Hide file tree
Showing 21 changed files with 194 additions and 60 deletions.
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;
}
4 changes: 2 additions & 2 deletions app/client/src/WidgetProvider/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ import type { Stylesheet } from "entities/AppTheming";
import { omit } from "lodash";
import moment from "moment";
import type { SVGProps } from "react";
import type { DerivedPropertiesMap } from "WidgetProvider/factory";
import type { WidgetFeatures } from "utils/WidgetFeatures";
import type { WidgetProps } from "../widgets/BaseWidget";
import type { ExtraDef } from "utils/autocomplete/defCreatorUtils";
import type { WidgetEntityConfig } from "ee/entities/DataTree/types";
import type {
WidgetQueryConfig,
Expand All @@ -27,6 +25,8 @@ import type {
Positioning,
ResponsiveBehavior,
} from "layoutSystems/common/utils/constants";
import type { DerivedPropertiesMap } from "./factory/types";
import type { ExtraDef } from "utils/autocomplete/types";

export interface WidgetSizeConfig {
viewportMinWidth: number;
Expand Down
7 changes: 6 additions & 1 deletion app/client/src/WidgetProvider/factory/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,16 @@ import type {
PasteDestinationInfo,
} from "layoutSystems/anvil/utils/paste/types";
import { call } from "redux-saga/effects";
import type { DerivedPropertiesMap } from "./types";

// exporting it as well so that existing imports are not affected
// TODO: remove this once all imports are updated
export type { DerivedPropertiesMap };

// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type WidgetDerivedPropertyType = any;
export type DerivedPropertiesMap = Record<string, string>;

export type WidgetType = (typeof WidgetFactory.widgetTypes)[number];

class WidgetFactory {
Expand Down
1 change: 1 addition & 0 deletions app/client/src/WidgetProvider/factory/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type DerivedPropertiesMap = Record<string, string>;
2 changes: 1 addition & 1 deletion app/client/src/constants/WidgetConstants.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SupportedLayouts } from "reducers/entityReducers/pageListReducer";
import type { SupportedLayouts } from "reducers/entityReducers/types";
import type { WidgetType as FactoryWidgetType } from "WidgetProvider/factory";
import { THEMEING_TEXT_SIZES } from "./ThemeConstants";
import type { WidgetCardProps } from "widgets/BaseWidget";
Expand Down
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 },
},
{
helpText: "Controls the visibility of the widget",
propertyName: "isVisible",
Expand Down
Loading

0 comments on commit 65df8d4

Please sign in to comment.