feat: Auto-height add reducers and actions (#17953)

* Add reducers for auto height feature (Internal Change, No changes reflected to users)
Co-authored-by: ankurrsinghal <ankur@appsmith.com>
This commit is contained in:
Abhinav Jha 2022-11-14 09:49:25 +05:30 committed by GitHub
parent 14159c5593
commit 3f71fa68a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 510 additions and 1 deletions

View File

@ -0,0 +1,52 @@
import {
ReduxActionTypes,
ReduxAction,
} from "@appsmith/constants/ReduxActionConstants";
import { TreeNode } from "utils/autoHeight/constants";
export interface UpdateWidgetAutoHeightPayload {
widgetId: string;
height: number;
}
export function setAutoHeightLayoutTreeAction(
tree: Record<string, TreeNode>,
canvasLevelMap: Record<string, number>,
) {
return {
type: ReduxActionTypes.SET_AUTO_HEIGHT_LAYOUT_TREE,
payload: { tree, canvasLevelMap },
};
}
export function generateAutoHeightLayoutTreeAction(
shouldCheckContainersForDynamicHeightUpdates: boolean,
layoutUpdated?: boolean,
) {
return {
type: ReduxActionTypes.GENERATE_AUTO_HEIGHT_LAYOUT_TREE,
payload: {
shouldCheckContainersForDynamicHeightUpdates,
layoutUpdated: !!layoutUpdated,
},
};
}
export function updateWidgetAutoHeightAction(
widgetId: string,
height: number,
): ReduxAction<UpdateWidgetAutoHeightPayload> {
return {
type: ReduxActionTypes.UPDATE_WIDGET_AUTO_HEIGHT,
payload: {
widgetId,
height,
},
};
}
export function checkContainersForAutoHeightAction() {
return {
type: ReduxActionTypes.CHECK_CONTAINERS_FOR_AUTO_HEIGHT,
};
}

View File

@ -2,6 +2,7 @@ import {
ReduxActionTypes,
ReduxAction,
} from "@appsmith/constants/ReduxActionConstants";
import { UpdateWidgetsPayload } from "reducers/entityReducers/canvasWidgetsReducer";
import { DynamicPath } from "utils/DynamicBindingUtils";
export const updateWidgetPropertyRequest = (
@ -74,6 +75,15 @@ export const setWidgetDynamicProperty = (
};
};
export const updateMultipleWidgetPropertiesAction = (
widgetsToUpdate: UpdateWidgetsPayload,
) => {
return {
type: ReduxActionTypes.UPDATE_MULTIPLE_WIDGET_PROPERTIES,
payload: widgetsToUpdate,
};
};
export interface UpdateWidgetPropertyRequestPayload {
widgetId: string;
propertyPath: string;

View File

@ -703,7 +703,14 @@ export const ReduxActionTypes = {
SET_JS_PANE_CONFIG_SELECTED_TAB: "SET_JS_PANE_CONFIG_SELECTED_TAB",
SET_JS_PANE_RESPONSE_SELECTED_TAB: "SET_JS_PANE_RESPONSE_SELECTED_TAB",
SET_JS_PANE_RESPONSE_PANE_HEIGHT: "SET_JS_PANE_RESPONSE_PANE_HEIGHT",
SET_AUTO_HEIGHT_LAYOUT_TREE: "SET_AUTO_HEIGHT_LAYOUT_TREE",
UPDATE_MULTIPLE_WIDGET_PROPERTIES: "UPDATE_MULTIPLE_WIDGET_PROPERTIES",
SET_CANVAS_LEVELS_MAP: "SET_CANVAS_LEVELS_MAP",
GENERATE_AUTO_HEIGHT_LAYOUT_TREE: "GENERATE_AUTO_HEIGHT_LAYOUT_TREE",
CHECK_CONTAINERS_FOR_AUTO_HEIGHT: "CHECK_CONTAINERS_FOR_AUTO_HEIGHT",
UPDATE_WIDGET_AUTO_HEIGHT: "UPDATE_WIDGET_AUTO_HEIGHT",
SET_LINT_ERRORS: "SET_LINT_ERRORS",
PROCESS_AUTO_HEIGHT_UPDATES: "PROCESS_AUTO_HEIGHT_UPDATES",
};
export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes];

View File

@ -66,6 +66,8 @@ import tenantReducer, {
} from "@appsmith/reducers/tenantReducer";
import { FocusHistoryState } from "reducers/uiReducers/focusHistoryReducer";
import { EditorContextState } from "reducers/uiReducers/editorContextReducer";
import { AutoHeightLayoutTreeReduxState } from "reducers/entityReducers/autoHeightReducers/autoHeightLayoutTreeReducer";
import { CanvasLevelsReduxState } from "reducers/entityReducers/autoHeightReducers/canvasLevelsReducer";
import { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
import lintErrorReducer from "reducers/lintingReducers";
@ -136,6 +138,8 @@ export interface AppState {
meta: MetaState;
app: AppDataState;
jsActions: JSCollectionDataState;
autoHeightLayoutTree: AutoHeightLayoutTreeReduxState;
canvasLevels: CanvasLevelsReduxState;
};
evaluations: {
tree: EvaluatedTreeState;

View File

@ -21,6 +21,10 @@ import {
triggerEvalOnMetaUpdate,
} from "actions/metaActions";
import {
checkContainersForAutoHeightAction,
updateWidgetAutoHeightAction,
} from "actions/autoHeightActions";
export type EditorContextType = {
executeAction?: (triggerPayload: ExecuteTriggerPayload) => void;
updateWidget?: (
@ -48,6 +52,8 @@ export type EditorContextType = {
propertyName: string,
propertyValue: any,
) => void;
updateWidgetAutoHeight?: (widgetId: string, height: number) => void;
checkContainersForAutoHeight?: () => void;
};
export const EditorContext: Context<EditorContextType> = createContext({});
@ -58,6 +64,7 @@ type EditorContextProviderProps = EditorContextType & {
function EditorContextProvider(props: EditorContextProviderProps) {
const {
batchUpdateWidgetProperty,
checkContainersForAutoHeight,
children,
deleteWidgetProperty,
disableDrag,
@ -66,6 +73,7 @@ function EditorContextProvider(props: EditorContextProviderProps) {
syncUpdateWidgetMetaProperty,
triggerEvalOnMetaUpdate,
updateWidget,
updateWidgetAutoHeight,
updateWidgetProperty,
} = props;
@ -82,6 +90,8 @@ function EditorContextProvider(props: EditorContextProviderProps) {
deleteWidgetProperty,
batchUpdateWidgetProperty,
triggerEvalOnMetaUpdate,
updateWidgetAutoHeight,
checkContainersForAutoHeight,
}),
[
executeAction,
@ -93,6 +103,8 @@ function EditorContextProvider(props: EditorContextProviderProps) {
deleteWidgetProperty,
batchUpdateWidgetProperty,
triggerEvalOnMetaUpdate,
updateWidgetAutoHeight,
checkContainersForAutoHeight,
],
);
return (
@ -121,6 +133,8 @@ const mapDispatchToProps = {
deleteWidgetProperty: deletePropertyAction,
batchUpdateWidgetProperty: batchUpdatePropertyAction,
triggerEvalOnMetaUpdate: triggerEvalOnMetaUpdate,
updateWidgetAutoHeight: updateWidgetAutoHeightAction,
checkContainersForAutoHeight: checkContainersForAutoHeightAction,
};
export default connect(null, mapDispatchToProps)(EditorContextProvider);

View File

@ -45,6 +45,11 @@ import { initAppViewer } from "actions/initActions";
import { WidgetGlobaStyles } from "globalStyles/WidgetGlobalStyles";
import { getAppsmithConfigs } from "@appsmith/configs";
import {
checkContainersForAutoHeightAction,
updateWidgetAutoHeightAction,
} from "actions/autoHeightActions";
const AppViewerBody = styled.section<{
hasPages: boolean;
headerHeight: number;
@ -227,6 +232,13 @@ function AppViewer(props: Props) {
[triggerEvalOnMetaUpdate, dispatch],
);
const updateWidgetAutoHeightCallback = useCallback(
(widgetId: string, height: number) => {
dispatch(updateWidgetAutoHeightAction(widgetId, height));
},
[updateWidgetAutoHeightAction, dispatch],
);
return (
<ThemeProvider theme={lightTheme}>
<EditorContext.Provider
@ -236,6 +248,8 @@ function AppViewer(props: Props) {
batchUpdateWidgetProperty: batchUpdateWidgetPropertyCallback,
syncUpdateWidgetMetaProperty: syncUpdateWidgetMetaPropertyCallback,
triggerEvalOnMetaUpdate: triggerEvalOnMetaUpdateCallback,
updateWidgetAutoHeight: updateWidgetAutoHeightCallback,
checkContainersForAutoHeight: checkContainersForAutoHeightAction,
}}
>
<WidgetGlobaStyles

View File

@ -0,0 +1,99 @@
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import reducer from "./autoHeightLayoutTreeReducer";
describe("Canvas Levels Reducer", () => {
it("should return the initial state", () => {
expect(reducer(undefined, { type: "", payload: "" })).toEqual({});
});
it("should set the new auto height layout tree", () => {
const type = ReduxActionTypes.SET_AUTO_HEIGHT_LAYOUT_TREE;
const payload = {
tree: {
0: {
aboves: [],
belows: [],
topRow: 0,
bottomRow: 0,
originalTopRow: 0,
originalBottomRow: 0,
},
},
};
const expected = {
0: {
aboves: [],
belows: [],
topRow: 0,
bottomRow: 0,
originalTopRow: 0,
originalBottomRow: 0,
},
};
expect(reducer(undefined, { type, payload })).toEqual(expected);
});
it("should augment the auto height layout tree further in the state", () => {
const type = ReduxActionTypes.SET_AUTO_HEIGHT_LAYOUT_TREE;
const payload = {
tree: {
0: {
aboves: [],
belows: [],
topRow: 0,
bottomRow: 0,
originalTopRow: 0,
originalBottomRow: 0,
},
},
};
const state = reducer(undefined, { type, payload });
const payload2 = {
tree: {
0: {
aboves: [],
belows: [1],
topRow: 0,
bottomRow: 0,
originalTopRow: 0,
originalBottomRow: 0,
},
1: {
aboves: [0],
belows: [],
topRow: 10,
bottomRow: 10,
originalTopRow: 10,
originalBottomRow: 10,
},
},
};
const expected = {
0: {
aboves: [],
belows: [1],
topRow: 0,
bottomRow: 0,
originalTopRow: 0,
originalBottomRow: 0,
},
1: {
aboves: [0],
belows: [],
topRow: 10,
bottomRow: 10,
originalTopRow: 10,
originalBottomRow: 10,
},
};
const state2 = reducer(state, { type, payload: payload2 });
expect(state2).toEqual(expected);
});
});

View File

@ -0,0 +1,56 @@
import { createImmerReducer } from "utils/ReducerUtils";
import {
ReduxAction,
ReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import { TreeNode } from "utils/autoHeight/constants";
import { xor } from "lodash";
export type AutoHeightLayoutTreePayload = {
tree: Record<string, TreeNode>;
canvasLevelMap: Record<string, number>;
};
export type AutoHeightLayoutTreeReduxState = {
[widgetId: string]: TreeNode & { level?: number };
};
const initialState: AutoHeightLayoutTreeReduxState = {};
const autoHeightLayoutTreeReducer = createImmerReducer(initialState, {
[ReduxActionTypes.SET_AUTO_HEIGHT_LAYOUT_TREE]: (
state: AutoHeightLayoutTreeReduxState,
action: ReduxAction<AutoHeightLayoutTreePayload>,
) => {
const { tree } = action.payload;
const diff = xor(Object.keys(state), ...Object.keys(tree));
for (const widgetId in diff) {
delete state[widgetId];
}
for (const widgetId in tree) {
if (state[widgetId]) {
const differentAboves = xor(
state[widgetId].aboves,
tree[widgetId].aboves,
);
if (differentAboves.length > 0) {
state[widgetId].aboves = tree[widgetId].aboves;
}
const differentBelows = xor(
state[widgetId].belows,
tree[widgetId].belows,
);
if (differentBelows.length > 0) {
state[widgetId].belows = tree[widgetId].belows;
}
state[widgetId].topRow = tree[widgetId].topRow;
state[widgetId].bottomRow = tree[widgetId].bottomRow;
} else {
state[widgetId] = tree[widgetId];
}
}
},
});
export default autoHeightLayoutTreeReducer;

View File

@ -0,0 +1,44 @@
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import reducer from "./canvasLevelsReducer";
describe("Canvas Levels Reducer", () => {
it("should return the initial state", () => {
expect(reducer(undefined, { type: "", payload: "" })).toEqual({});
});
it("should set the new canvas mappings to the state", () => {
const type = ReduxActionTypes.SET_CANVAS_LEVELS_MAP;
const payload = {
canvasLevelMap: {
0: 0,
},
};
const expected = {
0: 0,
};
expect(reducer(undefined, { type, payload })).toEqual(expected);
});
it("should augment the further canvas mappings to the state", () => {
const type = ReduxActionTypes.SET_CANVAS_LEVELS_MAP;
const payload = {
canvasLevelMap: {
0: 0,
},
};
const state = reducer(undefined, { type, payload });
const payload2 = {
canvasLevelMap: {
1: 1,
},
};
const expected = {
0: 0,
1: 1,
};
expect(reducer(state, { type, payload: payload2 })).toEqual(expected);
});
});

View File

@ -0,0 +1,29 @@
import { createImmerReducer } from "utils/ReducerUtils";
import {
ReduxAction,
ReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import { AutoHeightLayoutTreePayload } from "./autoHeightLayoutTreeReducer";
export type CanvasLevelsPayload = Record<string, number>;
export type CanvasLevelsReduxState = {
[widgetId: string]: number;
};
const initialState: CanvasLevelsReduxState = {};
const canvasLevelsReducer = createImmerReducer(initialState, {
[ReduxActionTypes.SET_CANVAS_LEVELS_MAP]: (
state: CanvasLevelsReduxState,
action: ReduxAction<AutoHeightLayoutTreePayload>,
) => {
const { canvasLevelMap } = action.payload;
for (const widgetId in canvasLevelMap) {
if (state[widgetId] !== canvasLevelMap[widgetId])
state[widgetId] = canvasLevelMap[widgetId];
}
},
});
export default canvasLevelsReducer;

View File

@ -0,0 +1,135 @@
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import reducer from "./canvasWidgetsReducer";
describe("Canvas Widgets Reducer", () => {
it("should return the initial state", () => {
expect(reducer(undefined, { type: "", payload: "" })).toEqual({});
});
it("should update the paths", () => {
const initialState = {
"0": { children: ["xyz123"] },
xyz123: {
bottomRow: 20,
topRow: 10,
someValue: {
apple: "orange",
},
},
};
const type = ReduxActionTypes.UPDATE_MULTIPLE_WIDGET_PROPERTIES;
const payload = {
xyz123: [
{
propertyPath: "someValue.apple",
propertyValue: "apple",
},
],
};
const expected = {
"0": { children: ["xyz123"] },
xyz123: {
bottomRow: 20,
topRow: 10,
someValue: {
apple: "apple",
},
},
};
expect(reducer(initialState, { type, payload })).toEqual(expected);
});
it("should create paths if they donot exist", () => {
const initialState = {
"0": { children: ["xyz123"] },
xyz123: {
bottomRow: 20,
topRow: 10,
someValue: {
apple: "orange",
},
},
};
const type = ReduxActionTypes.UPDATE_MULTIPLE_WIDGET_PROPERTIES;
const payload = {
xyz123: [
{
propertyPath: "someValue.games.ball",
propertyValue: ["football"],
},
],
};
const expected = {
"0": { children: ["xyz123"] },
xyz123: {
bottomRow: 20,
topRow: 10,
someValue: {
apple: "orange",
games: {
ball: ["football"],
},
},
},
};
expect(reducer(initialState, { type, payload })).toEqual(expected);
});
it("should not update the paths if the values are the same", () => {
const initialState = {
"0": { children: ["xyz123"] },
xyz123: {
bottomRow: 20,
topRow: 10,
someValue: {
apple: "orange",
},
},
};
const type = ReduxActionTypes.UPDATE_MULTIPLE_WIDGET_PROPERTIES;
const payload = {
xyz123: [
{
propertyPath: "someValue.apple",
propertyValue: "orange",
},
],
};
// Reference equality check using toBe
expect(reducer(initialState, { type, payload })).toBe(initialState);
});
it("should have the same reference for paths not updated", () => {
const initialState = {
"0": { children: ["xyz123"] },
xyz123: {
bottomRow: 20,
topRow: 10,
someValue: {
apple: "orange",
games: {
ball: ["football"],
},
},
},
};
const type = ReduxActionTypes.UPDATE_MULTIPLE_WIDGET_PROPERTIES;
const payload = {
xyz123: [
{
propertyPath: "someValue.apple",
propertyValue: "orange",
},
],
};
const result = reducer(initialState, { type, payload }).xyz123.someValue
.games;
// Reference equality check using toBe
expect(result).toBe(initialState.xyz123.someValue.games);
});
});

View File

@ -6,10 +6,23 @@ import {
} from "@appsmith/constants/ReduxActionConstants";
import { WidgetProps } from "widgets/BaseWidget";
import { Diff, diff } from "deep-diff";
import { uniq } from "lodash";
import { uniq, get, set } from "lodash";
const initialState: CanvasWidgetsReduxState = {};
/* This type is an object whose keys are widgetIds and values are arrays with property paths
and property values
For example:
{ "xyz123": [{ propertyPath: "bottomRow", propertyValue: 20 }] }
*/
export type UpdateWidgetsPayload = Record<
string,
Array<{
propertyPath: string;
propertyValue: unknown;
}>
>;
export type FlattenedWidgetProps<orType = never> =
| (WidgetProps & {
children?: string[];
@ -69,6 +82,26 @@ const canvasWidgetsReducer = createImmerReducer(initialState, {
}
}
},
[ReduxActionTypes.UPDATE_MULTIPLE_WIDGET_PROPERTIES]: (
state: CanvasWidgetsReduxState,
action: ReduxAction<UpdateWidgetsPayload>,
) => {
// For each widget whose properties we would like to update
for (const [widgetId, propertyPathsToUpdate] of Object.entries(
action.payload,
)) {
// Iterate through each property to update in `widgetId`
propertyPathsToUpdate.forEach(({ propertyPath, propertyValue }) => {
const path = `${widgetId}.${propertyPath}`;
// Get original value in reducer
const originalPropertyValue = get(state, path);
// If the original and new values are different
if (propertyValue !== originalPropertyValue)
// Set the new values
set(state, path, propertyValue);
});
}
},
});
export interface CanvasWidgetsReduxState {
[widgetId: string]: FlattenedWidgetProps;

View File

@ -10,6 +10,8 @@ import metaReducer from "./metaReducer";
import pageListReducer from "./pageListReducer";
import pluginsReducer from "reducers/entityReducers/pluginsReducer";
import widgetConfigReducer from "./widgetConfigReducer";
import autoHeightLayoutTreeReducer from "./autoHeightReducers/autoHeightLayoutTreeReducer";
import canvasLevelsReducer from "./autoHeightReducers/canvasLevelsReducer";
const entityReducer = combineReducers({
canvasWidgets: canvasWidgetsReducer,
@ -23,6 +25,8 @@ const entityReducer = combineReducers({
meta: metaReducer,
app: appReducer,
jsActions: jsActionsReducer,
autoHeightLayoutTree: autoHeightLayoutTreeReducer,
canvasLevels: canvasLevelsReducer,
});
export default entityReducer;

View File

@ -0,0 +1,8 @@
export type TreeNode = {
aboves: string[];
belows: string[];
topRow: number;
bottomRow: number;
originalTopRow: number;
originalBottomRow: number;
};