chore: hydrate widgets properties from session on drop (#32934)

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"

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai
--><!-- This is an auto-generated comment: Cypress test results -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/8936335019>
> Commit: fbe155c1165c7e99d837474a7baf56b1430e880a
> Cypress dashboard url: <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=8936335019&attempt=1"
target="_blank">Click here!</a>

<!-- end of auto-generated comment: Cypress test results  -->

---------

Co-authored-by: Pawan Kumar <pawankumar@Pawans-MacBook-Pro-2.local>
This commit is contained in:
Pawan Kumar 2024-05-06 17:10:56 +05:30 committed by GitHub
parent a5edfb025a
commit 905e5b7338
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 163 additions and 10 deletions

View File

@ -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",
};
}
}

View File

@ -114,6 +114,8 @@ export interface PropertyPaneControlConfig {
*/
controlConfig?: Record<string, unknown>;
defaultValue?: unknown;
/** used to mark a property as reusable so that it can be reused in next dropping widget */
isReusable?: boolean;
}
interface ValidationConfigParams {

View File

@ -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,
});
}
},
[

View File

@ -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,
);
}
}

View File

@ -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;
}

View File

@ -17,6 +17,7 @@ export const propertyPaneStyle = [
validation: {
type: ValidationTypes.BOOLEAN,
},
isReusable: true,
},
],
},

View File

@ -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: {

View File

@ -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: {

View File

@ -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: {

View File

@ -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 },
},
],

View File

@ -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,

View File

@ -48,6 +48,7 @@ export const defaultsConfig = {
widgetId: "",
id: "separator",
index: 2,
label: "Separator",
itemType: "SEPARATOR",
},
button4: {