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:
parent
a5edfb025a
commit
905e5b7338
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export const propertyPaneStyle = [
|
|||
validation: {
|
||||
type: ValidationTypes.BOOLEAN,
|
||||
},
|
||||
isReusable: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export const defaultsConfig = {
|
|||
widgetId: "",
|
||||
id: "separator",
|
||||
index: 2,
|
||||
label: "Separator",
|
||||
itemType: "SEPARATOR",
|
||||
},
|
||||
button4: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user