From 905e5b7338e243aa3670c1f5ccada6da93cc55ae Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Mon, 6 May 2024 17:10:56 +0530 Subject: [PATCH] chore: hydrate widgets properties from session on drop (#32934) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds a ability for anvil widgets ( mainly buttons, icon buttons, heading, paragraph and inline buttons ) to use values from session on creation on drop. /ok-to-test tags="@tag.Anvil" ## Summary by CodeRabbit - **New Features** - Introduced logic to maintain button appearance using session storage values. - Added functionality to mark properties as reusable, enhancing widget configuration flexibility. - **Bug Fixes** - Corrected a syntax error in `WidgetCard.tsx` to ensure proper class name generation. - **Enhancements** - Improved widget creation process by saving and retrieving properties from session storage, ensuring consistency across sessions. > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: fbe155c1165c7e99d837474a7baf56b1430e880a > Cypress dashboard url: Click here! --------- Co-authored-by: Pawan Kumar --- .../propertyControls/ButtonListControl.tsx | 27 ++++++--- .../constants/PropertyControlConstants.tsx | 2 + .../Editor/PropertyPane/PropertyControl.tsx | 14 +++++ .../src/pages/Editor/PropertyPane/helpers.tsx | 57 ++++++++++++++++++ app/client/src/sagas/WidgetAdditionSagas.ts | 60 +++++++++++++++++++ .../widget/config/propertyPaneStyle.ts | 1 + .../config/propertyPaneConfig/styleConfig.ts | 2 + .../config/propertyPaneConfig/styleConfig.ts | 2 + .../propertyPaneConfig/contentConfig.ts | 2 + .../config/propertyPaneConfig/styleConfig.ts | 3 + .../propertyPaneConfig/contentConfig.ts | 2 +- .../config/defaultsConfig.ts | 1 + 12 files changed, 163 insertions(+), 10 deletions(-) diff --git a/app/client/src/components/propertyControls/ButtonListControl.tsx b/app/client/src/components/propertyControls/ButtonListControl.tsx index 7b483d4338..1243d1cf7c 100644 --- a/app/client/src/components/propertyControls/ButtonListControl.tsx +++ b/app/client/src/components/propertyControls/ButtonListControl.tsx @@ -2,7 +2,6 @@ import React from "react"; import type { ControlProps } from "./BaseControl"; import BaseControl from "./BaseControl"; import { generateReactKey } from "utils/generators"; -import { getNextEntityName } from "utils/AppsmithUtils"; import orderBy from "lodash/orderBy"; import isString from "lodash/isString"; import isUndefined from "lodash/isUndefined"; @@ -178,17 +177,13 @@ class ButtonListControl extends BaseControl< let groupButtons = this.props.propertyValue; const groupButtonsArray = this.getMenuItems(); const newGroupButtonId = generateReactKey({ prefix: "groupButton" }); - const newGroupButtonLabel = getNextEntityName( - "Group Button ", - groupButtonsArray.map((groupButton: any) => groupButton.label), - ); groupButtons = { ...groupButtons, [newGroupButtonId]: { id: newGroupButtonId, index: groupButtonsArray.length, - label: isSeparator ? "Separator" : newGroupButtonLabel, + label: isSeparator ? "Separator" : "Do Something", widgetId: generateReactKey(), isDisabled: false, itemType: isSeparator ? "SEPARATOR" : "BUTTON", @@ -216,9 +211,23 @@ class ButtonListControl extends BaseControl< }; } - // if the widget is a WDS_INLINE_BUTTONS_WIDGET, and button already have filled button variant in groupButtons, - // then we should add a secondary button ( outlined button ) instead of simple button if (this.props.widgetProperties.type === "WDS_INLINE_BUTTONS_WIDGET") { + // if buttonVariant and buttonColor values ar present in session storage, then we should use those values + const buttonVariantSessionValue = sessionStorage.getItem( + "WDS_INLINE_BUTTONS_WIDGET.buttonVariant", + ); + const buttonColorSessionValue = sessionStorage.getItem( + "WDS_INLINE_BUTTONS_WIDGET.buttonColor", + ); + + groupButtons[newGroupButtonId] = { + ...groupButtons[newGroupButtonId], + buttonVariant: buttonVariantSessionValue || "filled", + buttonColor: buttonColorSessionValue || "accent", + }; + + // if the widget is a WDS_INLINE_BUTTONS_WIDGET, and button already have filled button variant in groupButtons, + // then we should add a secondary button ( outlined button ) instead of simple button const filledButtonVariant = groupButtonsArray.find( (groupButton: any) => groupButton.buttonVariant === "filled", ); @@ -226,7 +235,7 @@ class ButtonListControl extends BaseControl< if (filledButtonVariant) { groupButtons[newGroupButtonId] = { ...groupButtons[newGroupButtonId], - buttonVariant: "outlined", + buttonVariant: buttonVariantSessionValue || "outlined", }; } } diff --git a/app/client/src/constants/PropertyControlConstants.tsx b/app/client/src/constants/PropertyControlConstants.tsx index d7b438eceb..5839a35db8 100644 --- a/app/client/src/constants/PropertyControlConstants.tsx +++ b/app/client/src/constants/PropertyControlConstants.tsx @@ -114,6 +114,8 @@ export interface PropertyPaneControlConfig { */ controlConfig?: Record; defaultValue?: unknown; + /** used to mark a property as reusable so that it can be reused in next dropping widget */ + isReusable?: boolean; } interface ValidationConfigParams { diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx index 3e1f68f210..b3c9645adb 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyControl.tsx @@ -62,6 +62,8 @@ import type { PropertyUpdates } from "WidgetProvider/constants"; import { getIsOneClickBindingOptionsVisibility } from "selectors/oneClickBindingSelectors"; import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag"; +import { savePropertyInSessionStorageIfRequired } from "./helpers"; +import { getParentWidget } from "selectors/widgetSelectors"; const ResetIcon = importSvg( async () => import("assets/icons/control/undo_2.svg"), @@ -99,6 +101,9 @@ const PropertyControl = memo((props: Props) => { ); const widgetProperties: WidgetProperties = useSelector(propsSelector, equal); + const parentWidget = useSelector((state) => + getParentWidget(state, widgetProperties.widgetId), + ); // get the dataTreePath and apply enhancement if exists let dataTreePath: string | undefined = @@ -574,6 +579,15 @@ const PropertyControl = memo((props: Props) => { // updating properties of a widget(s) should be done only once when property value changes. // to make sure dsl updates are atomic which is a necessity for undo/redo. onBatchUpdatePropertiesOfMultipleWidgets(allPropertiesToUpdates); + + savePropertyInSessionStorageIfRequired({ + isReusable: !!props.isReusable, + widgetProperties, + propertyName, + propertyValue, + parentWidgetId: parentWidget?.widgetId, + parentWidgetType: parentWidget?.type, + }); } }, [ diff --git a/app/client/src/pages/Editor/PropertyPane/helpers.tsx b/app/client/src/pages/Editor/PropertyPane/helpers.tsx index a335d4b8f0..3cf9a5fe57 100644 --- a/app/client/src/pages/Editor/PropertyPane/helpers.tsx +++ b/app/client/src/pages/Editor/PropertyPane/helpers.tsx @@ -11,6 +11,7 @@ import { Callout } from "design-system"; import React from "react"; import WidgetFactory from "WidgetProvider/factory"; import type { WidgetCallout } from "WidgetProvider/constants"; +import { isDynamicValue } from "utils/DynamicBindingUtils"; export function useSearchText(initialVal: string) { const [searchText, setSearchText] = useState(initialVal); @@ -127,3 +128,59 @@ export function renderWidgetCallouts(props: WidgetProps): JSX.Element[] { return []; } } + +/** + * saves property value incase it is a reusable property in the session storage so that we can re-use + * the property value when we create widget on drop. + * + * Note: these values that we are storing will be used in widgetAddtionSaga to hydrate the widget properties when + * we create widget on drop + */ +export function savePropertyInSessionStorageIfRequired(props: { + isReusable: boolean; + widgetProperties: any; + propertyName: string; + propertyValue: string; + parentWidgetId?: string; + parentWidgetType?: string; +}) { + const { + isReusable, + parentWidgetId, + parentWidgetType, + propertyName, + propertyValue, + widgetProperties, + } = props; + + if (isReusable && isDynamicValue(propertyValue) === false) { + let widgetType = widgetProperties.type; + let widgetPropertyName = propertyName; + + // in case of type is WDS_ICON_BUTTON_WIDGET, we need to use key WDS_BUTTON_WIDGET, reason being + // we want to reuse the property values of icon button for button as well when we create button widget on drop + if (widgetType === "WDS_ICON_BUTTON_WIDGET") { + widgetType = "WDS_BUTTON_WIDGET"; + } + + // in case of type is WDS_INLINE_BUTTONS_WIDGET, we want to just store the property that is being changed, not the whole property path + if (widgetType === "WDS_INLINE_BUTTONS_WIDGET") { + widgetPropertyName = propertyName.split(".").pop() as string; + } + + // if case of type is ZONE_WIDGET, we need to store the property value with parent widget id as well + // parent id is required because we want to hydrate value of property of the new zone widget only if the parent widget is same + if (widgetType === "ZONE_WIDGET") { + if (!(parentWidgetType && parentWidgetId)) { + return; + } + + widgetPropertyName = `${parentWidgetId}.${widgetPropertyName}`; + } + + sessionStorage.setItem( + `${widgetType}.${widgetPropertyName}`, + propertyValue, + ); + } +} diff --git a/app/client/src/sagas/WidgetAdditionSagas.ts b/app/client/src/sagas/WidgetAdditionSagas.ts index ae51297db2..47e4d25271 100644 --- a/app/client/src/sagas/WidgetAdditionSagas.ts +++ b/app/client/src/sagas/WidgetAdditionSagas.ts @@ -53,6 +53,7 @@ import { import { getPropertiesToUpdate } from "./WidgetOperationSagas"; import { getWidget, getWidgets } from "./selectors"; import { addBuildingBlockToCanvasSaga } from "./BuildingBlockAdditionSagas"; +import { getCurrentlyOpenAnvilDetachedWidgets } from "layoutSystems/anvil/integrations/modalSelectors"; const WidgetTypes = WidgetFactory.widgetTypes; @@ -86,8 +87,13 @@ function* getChildWidgetProps( ]); const themeDefaultConfig = WidgetFactory.getWidgetStylesheetConfigMap(type) || {}; + const widgetSessionValues = getWidgetSessionValues(type, parent); const mainCanvasWidth: number = yield select(getCanvasWidth); const isMobile: boolean = yield select(getIsAutoLayoutMobileBreakPoint); + const detachedWidgets: string[] = yield select( + getCurrentlyOpenAnvilDetachedWidgets, + ); + const isModalOpen = detachedWidgets && detachedWidgets.length > 0; if (!widgetName) { const widgetNames = Object.keys(widgets).map((w) => widgets[w].widgetName); @@ -116,6 +122,12 @@ function* getChildWidgetProps( } } + // in case we are creating zone inside zone, we want to use the parent's column space, we want + // to make sure the elevateBackground is set to false + if (type === "ZONE_WIDGET" && isModalOpen) { + props = { ...props, elevatedBackground: false }; + } + const isAutoLayout = isStack(widgets, parent); const isFillWidget = restDefaultConfig?.responsiveBehavior === ResponsiveBehavior.Fill; @@ -130,6 +142,7 @@ function* getChildWidgetProps( widgetId: newWidgetId, renderMode: RenderModes.CANVAS, ...themeDefaultConfig, + ...widgetSessionValues, }; const { minWidth } = getWidgetMinMaxDimensionsInPixel( @@ -516,3 +529,50 @@ export default function* widgetAdditionSagas() { takeEvery(ReduxActionTypes.WIDGET_ADD_NEW_TAB_CHILD, addNewTabChildSaga), ]); } + +/** + * retrieves the values from session storage for the widget properties + * for hydration of the widget when we create widget on drop + */ +export function getWidgetSessionValues( + type: string, + parent: FlattenedWidgetProps, +) { + // For WDS_INLINE_BUTTONS_WIDGET, we want to hydation only to work when we add more items to the inline button group. + // So we don't want to hydrate the values when we drop the widget on the canvas. + if (["WDS_INLINE_BUTTONS_WIDGET"].includes(type)) return; + + let widgetType = type; + const configMap = WidgetFactory.widgetConfigMap.get(type); + const widgetSessionValues: any = {}; + + // in case we are dropping WDS_ICON_BUTTON_WIDGET, we want to reuse the values of BUTTON_WIDGET + if (type === "WDS_ICON_BUTTON_WIDGET") { + widgetType = "WDS_BUTTON_WIDGET"; + } + + for (const key in configMap) { + if (configMap[key]) { + let sessionStorageKey = `${widgetType}.${key}`; + + if (type === "ZONE_WIDGET") { + sessionStorageKey = `${widgetType}.${parent.widgetId}.${key}`; + } + + let valueFromSession: any = sessionStorage.getItem(sessionStorageKey); + + // parse "true" as true and "false" as false + if (valueFromSession === "true") { + valueFromSession = true; + } else if (valueFromSession === "false") { + valueFromSession = false; + } + + if (valueFromSession !== undefined && valueFromSession !== null) { + widgetSessionValues[key] = valueFromSession; + } + } + } + + return widgetSessionValues; +} diff --git a/app/client/src/widgets/anvil/ZoneWidget/widget/config/propertyPaneStyle.ts b/app/client/src/widgets/anvil/ZoneWidget/widget/config/propertyPaneStyle.ts index a8ccfd337e..41afd3986e 100644 --- a/app/client/src/widgets/anvil/ZoneWidget/widget/config/propertyPaneStyle.ts +++ b/app/client/src/widgets/anvil/ZoneWidget/widget/config/propertyPaneStyle.ts @@ -17,6 +17,7 @@ export const propertyPaneStyle = [ validation: { type: ValidationTypes.BOOLEAN, }, + isReusable: true, }, ], }, diff --git a/app/client/src/widgets/wds/WDSButtonWidget/config/propertyPaneConfig/styleConfig.ts b/app/client/src/widgets/wds/WDSButtonWidget/config/propertyPaneConfig/styleConfig.ts index 671f0acf1f..94c5fb498e 100644 --- a/app/client/src/widgets/wds/WDSButtonWidget/config/propertyPaneConfig/styleConfig.ts +++ b/app/client/src/widgets/wds/WDSButtonWidget/config/propertyPaneConfig/styleConfig.ts @@ -19,6 +19,7 @@ export const propertyPaneStyleConfig = [ isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, + isReusable: true, validation: { type: ValidationTypes.TEXT, params: { @@ -40,6 +41,7 @@ export const propertyPaneStyleConfig = [ isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, + isReusable: true, validation: { type: ValidationTypes.TEXT, params: { diff --git a/app/client/src/widgets/wds/WDSIconButtonWidget/config/propertyPaneConfig/styleConfig.ts b/app/client/src/widgets/wds/WDSIconButtonWidget/config/propertyPaneConfig/styleConfig.ts index 34c2dd186f..ffe6686f36 100644 --- a/app/client/src/widgets/wds/WDSIconButtonWidget/config/propertyPaneConfig/styleConfig.ts +++ b/app/client/src/widgets/wds/WDSIconButtonWidget/config/propertyPaneConfig/styleConfig.ts @@ -19,6 +19,7 @@ export const propertyPaneStyleConfig = [ isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, + isReusable: true, validation: { type: ValidationTypes.TEXT, params: { @@ -40,6 +41,7 @@ export const propertyPaneStyleConfig = [ isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, + isReusable: true, validation: { type: ValidationTypes.TEXT, params: { diff --git a/app/client/src/widgets/wds/WDSInlineButtonsWidget/config/propertyPaneConfig/contentConfig.ts b/app/client/src/widgets/wds/WDSInlineButtonsWidget/config/propertyPaneConfig/contentConfig.ts index d8cdd4fc72..5499d76b3d 100644 --- a/app/client/src/widgets/wds/WDSInlineButtonsWidget/config/propertyPaneConfig/contentConfig.ts +++ b/app/client/src/widgets/wds/WDSInlineButtonsWidget/config/propertyPaneConfig/contentConfig.ts @@ -104,6 +104,7 @@ export const propertyPaneContentConfig = [ isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, + isReusable: true, validation: { type: ValidationTypes.TEXT, params: { @@ -125,6 +126,7 @@ export const propertyPaneContentConfig = [ isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, + isReusable: true, validation: { type: ValidationTypes.TEXT, params: { diff --git a/app/client/src/widgets/wds/WDSParagraphWidget/config/propertyPaneConfig/styleConfig.ts b/app/client/src/widgets/wds/WDSParagraphWidget/config/propertyPaneConfig/styleConfig.ts index 75380a1ee0..bf3dee16ed 100644 --- a/app/client/src/widgets/wds/WDSParagraphWidget/config/propertyPaneConfig/styleConfig.ts +++ b/app/client/src/widgets/wds/WDSParagraphWidget/config/propertyPaneConfig/styleConfig.ts @@ -32,6 +32,7 @@ export const propertyPaneStyleConfig = [ isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, + isReusable: true, validation: { type: ValidationTypes.TEXT, params: { @@ -69,6 +70,7 @@ export const propertyPaneStyleConfig = [ isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, + isReusable: true, validation: { type: ValidationTypes.TEXT, params: { @@ -95,6 +97,7 @@ export const propertyPaneStyleConfig = [ isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, + isReusable: true, validation: { type: ValidationTypes.TEXT }, }, ], diff --git a/app/client/src/widgets/wds/WDSRadioGroupWidget/config/propertyPaneConfig/contentConfig.ts b/app/client/src/widgets/wds/WDSRadioGroupWidget/config/propertyPaneConfig/contentConfig.ts index 4aa6d297be..6d20b7827d 100644 --- a/app/client/src/widgets/wds/WDSRadioGroupWidget/config/propertyPaneConfig/contentConfig.ts +++ b/app/client/src/widgets/wds/WDSRadioGroupWidget/config/propertyPaneConfig/contentConfig.ts @@ -36,7 +36,7 @@ export const propertyPaneContentConfig = [ helpText: "Sets a default selected option", propertyName: "defaultOptionValue", label: "Default selected value", - placeholderText: "Y", + placeholderText: "L", controlType: "INPUT_TEXT", isBindProperty: true, isTriggerProperty: false, diff --git a/app/client/src/widgets/wds/WDSToolbarButtonsWidget/config/defaultsConfig.ts b/app/client/src/widgets/wds/WDSToolbarButtonsWidget/config/defaultsConfig.ts index dae849ad8a..bf29c3f71f 100644 --- a/app/client/src/widgets/wds/WDSToolbarButtonsWidget/config/defaultsConfig.ts +++ b/app/client/src/widgets/wds/WDSToolbarButtonsWidget/config/defaultsConfig.ts @@ -48,6 +48,7 @@ export const defaultsConfig = { widgetId: "", id: "separator", index: 2, + label: "Separator", itemType: "SEPARATOR", }, button4: {