diff --git a/app/client/packages/design-system/widgets/src/components/Button/src/types.ts b/app/client/packages/design-system/widgets/src/components/Button/src/types.ts index 1c4e9649ffe2..fd181f5e16ff 100644 --- a/app/client/packages/design-system/widgets/src/components/Button/src/types.ts +++ b/app/client/packages/design-system/widgets/src/components/Button/src/types.ts @@ -41,4 +41,8 @@ export interface ButtonProps extends HeadlessButtonProps { * @default medium */ size?: Omit; + /** 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; } diff --git a/app/client/src/WidgetProvider/constants.ts b/app/client/src/WidgetProvider/constants.ts index 3183d918c0c1..10aee50f74eb 100644 --- a/app/client/src/WidgetProvider/constants.ts +++ b/app/client/src/WidgetProvider/constants.ts @@ -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, @@ -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; diff --git a/app/client/src/WidgetProvider/factory/index.tsx b/app/client/src/WidgetProvider/factory/index.tsx index d159b3820816..26e55f2a3f9f 100644 --- a/app/client/src/WidgetProvider/factory/index.tsx +++ b/app/client/src/WidgetProvider/factory/index.tsx @@ -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; + export type WidgetType = (typeof WidgetFactory.widgetTypes)[number]; class WidgetFactory { diff --git a/app/client/src/WidgetProvider/factory/types.ts b/app/client/src/WidgetProvider/factory/types.ts new file mode 100644 index 000000000000..6de75263a7d1 --- /dev/null +++ b/app/client/src/WidgetProvider/factory/types.ts @@ -0,0 +1 @@ +export type DerivedPropertiesMap = Record; diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index f5ff85758468..c50281edd1a8 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -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"; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/component/RecaptchaV2.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/component/RecaptchaV2.tsx index 1b5acfd8c604..15faeb4d24f7 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/component/RecaptchaV2.tsx +++ b/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/component/RecaptchaV2.tsx @@ -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 @@ -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 diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/component/RecaptchaV3.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/component/RecaptchaV3.tsx index d931a3dcfbd3..47af90854790 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/component/RecaptchaV3.tsx +++ b/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/component/RecaptchaV3.tsx @@ -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 @@ -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(() => { diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/component/index.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/component/index.tsx index c9cccf9906b2..5be6c7518791 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/component/index.tsx +++ b/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/component/index.tsx @@ -1,10 +1,11 @@ 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; @@ -12,17 +13,26 @@ export interface ButtonComponentProps extends ButtonProps { 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 ( - diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/component/useRecaptcha.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/component/useRecaptcha.tsx index 22cd29639c00..7eda6ac2a636 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/component/useRecaptcha.tsx +++ b/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/component/useRecaptcha.tsx @@ -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 @@ -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") { diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/config/defaultsConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/config/defaultsConfig.ts index afb0ed2fa914..78fb054cfa9b 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/config/defaultsConfig.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/config/defaultsConfig.ts @@ -15,7 +15,7 @@ export const defaultsConfig = { widgetName: "Button", isDisabled: false, isVisible: true, - disabledWhenInvalid: false, + disableOnInvalidForm: false, resetFormOnClick: false, recaptchaType: RecaptchaTypes.V3, version: 1, diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/config/propertyPaneConfig/contentConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/config/propertyPaneConfig/contentConfig.ts index 5fe241113430..9613a6eaca79 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/config/propertyPaneConfig/contentConfig.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/config/propertyPaneConfig/contentConfig.ts @@ -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 }, + }, + ], + }, ]; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/widget/index.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/widget/index.tsx index 897be61ab0f6..e0e1666f219f 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/widget/index.tsx +++ b/app/client/src/modules/ui-builder/ui/wds/WDSButtonWidget/widget/index.tsx @@ -70,7 +70,7 @@ class WDSButtonWidget extends BaseWidget { return config.settersConfig; } - onButtonClick = () => { + onButtonClick = (onReset?: () => void) => { if (this.props.onClick) { this.setState({ isLoading: true }); @@ -79,33 +79,35 @@ class WDSButtonWidget extends BaseWidget { 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), }, }); }; @@ -124,49 +126,34 @@ class WDSButtonWidget extends BaseWidget { } }; - 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 ( { + Omit { text?: string; isVisible?: boolean; isDisabled?: boolean; @@ -17,4 +17,5 @@ export interface ButtonWidgetProps googleRecaptchaKey?: string; recaptchaType?: RecaptchaType; disabledWhenInvalid?: boolean; + onClick?: string; } diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSZoneWidget/widget/config/defaultConfig.ts b/app/client/src/modules/ui-builder/ui/wds/WDSZoneWidget/widget/config/defaultConfig.ts index 266c26f18cca..2d910d7fa52e 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSZoneWidget/widget/config/defaultConfig.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSZoneWidget/widget/config/defaultConfig.ts @@ -23,6 +23,7 @@ export const defaultConfig: WidgetDefaultProps = { version: 1, widgetName: "Zone", isVisible: true, + useAsForm: false, blueprint: { operations: [ { diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSZoneWidget/widget/config/propertyPaneContent.ts b/app/client/src/modules/ui-builder/ui/wds/WDSZoneWidget/widget/config/propertyPaneContent.ts index a826a3ea5021..143ee1566ebc 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSZoneWidget/widget/config/propertyPaneContent.ts +++ b/app/client/src/modules/ui-builder/ui/wds/WDSZoneWidget/widget/config/propertyPaneContent.ts @@ -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", diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSZoneWidget/widget/context.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSZoneWidget/widget/context.tsx new file mode 100644 index 000000000000..ec9a4af9ffeb --- /dev/null +++ b/app/client/src/modules/ui-builder/ui/wds/WDSZoneWidget/widget/context.tsx @@ -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); + 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 ( + + {props.children} + + ); +}; + +export default WDSZoneWidgetContext; diff --git a/app/client/src/modules/ui-builder/ui/wds/WDSZoneWidget/widget/index.tsx b/app/client/src/modules/ui-builder/ui/wds/WDSZoneWidget/widget/index.tsx index 3c252e8d9102..04e99fafd716 100644 --- a/app/client/src/modules/ui-builder/ui/wds/WDSZoneWidget/widget/index.tsx +++ b/app/client/src/modules/ui-builder/ui/wds/WDSZoneWidget/widget/index.tsx @@ -34,6 +34,7 @@ import type { import { call } from "redux-saga/effects"; import { pasteWidgetsInZone } from "layoutSystems/anvil/utils/paste/zonePasteUtils"; import { SectionColumns } from "layoutSystems/anvil/sectionSpaceDistributor/constants"; +import { WDSZoneWidgetContextProvider } from "./context"; class WDSZoneWidget extends BaseWidget { static type = anvilWidgets.ZONE_WIDGET; @@ -143,6 +144,10 @@ class WDSZoneWidget extends BaseWidget { return res; } + onReset = () => { + this.resetChildrenMetaProperty(this.props.widgetId); + }; + getWidgetView(): ReactNode { return ( { elevation={Elevations.ZONE_ELEVATION} {...this.props} > - + + + ); } @@ -158,6 +169,7 @@ class WDSZoneWidget extends BaseWidget { export interface WDSZoneWidgetProps extends ContainerWidgetProps { layout: LayoutProps[]; + useAsForm?: boolean; } export default WDSZoneWidget; diff --git a/app/client/src/reducers/entityReducers/pageListReducer.tsx b/app/client/src/reducers/entityReducers/pageListReducer.tsx index 7ad83177412d..8995062a38d1 100644 --- a/app/client/src/reducers/entityReducers/pageListReducer.tsx +++ b/app/client/src/reducers/entityReducers/pageListReducer.tsx @@ -16,6 +16,11 @@ import { sortBy } from "lodash"; import type { DSL } from "reducers/uiReducers/pageCanvasStructureReducer"; import { createReducer } from "utils/ReducerUtils"; import type { Page } from "entities/Page"; +import type { SupportedLayouts } from "./types"; + +// exporting it as well so that existing imports are not affected +// TODO: remove this once all imports are updated +export type { SupportedLayouts }; const initialState: PageListReduxState = { pages: [], @@ -313,13 +318,6 @@ export const pageListReducer = createReducer(initialState, { }, }); -export type SupportedLayouts = - | "DESKTOP" - | "TABLET_LARGE" - | "TABLET" - | "MOBILE" - | "FLUID"; - export interface AppLayoutConfig { type: SupportedLayouts; } diff --git a/app/client/src/reducers/entityReducers/types.ts b/app/client/src/reducers/entityReducers/types.ts new file mode 100644 index 000000000000..dfddb2a6eac0 --- /dev/null +++ b/app/client/src/reducers/entityReducers/types.ts @@ -0,0 +1,6 @@ +export type SupportedLayouts = + | "DESKTOP" + | "TABLET_LARGE" + | "TABLET" + | "MOBILE" + | "FLUID"; diff --git a/app/client/src/utils/autocomplete/defCreatorUtils.ts b/app/client/src/utils/autocomplete/defCreatorUtils.ts index 409f5f7a6527..a8bdab67c4f7 100644 --- a/app/client/src/utils/autocomplete/defCreatorUtils.ts +++ b/app/client/src/utils/autocomplete/defCreatorUtils.ts @@ -7,8 +7,9 @@ import type { Def } from "tern"; import { Types, getType } from "utils/TypeHelpers"; import { shouldAddSetter } from "workers/Evaluation/evaluate"; import { typeToTernType } from "workers/common/JSLibrary/ternDefinitionGenerator"; +import type { ExtraDef } from "./types"; -export type ExtraDef = Record; +export type { ExtraDef }; export const flattenDef = (def: Def, entityName: string): Def => { const flattenedDef = def; diff --git a/app/client/src/utils/autocomplete/types.ts b/app/client/src/utils/autocomplete/types.ts index 538163a93d53..0a054c4db8fd 100644 --- a/app/client/src/utils/autocomplete/types.ts +++ b/app/client/src/utils/autocomplete/types.ts @@ -1,3 +1,5 @@ +import type { Def } from "tern"; + export enum TernWorkerAction { INIT = "INIT", ADD_FILE = "ADD_FILE", @@ -12,3 +14,5 @@ export enum TernWorkerAction { // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any export type CallbackFn = (...args: any) => any; + +export type ExtraDef = Record;