Skip to content

Commit 034bba2

Browse files
committed
Merge branch 'release' of https://github.com/appsmithorg/appsmith into feat/implement-infinite-scroll-with-dynamic-height
2 parents 3d3060e + a619db5 commit 034bba2

File tree

14 files changed

+160
-21
lines changed

14 files changed

+160
-21
lines changed

app/client/cypress/fixtures/customWidget.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"srcDoc": {
2727
"html": "<!-- no need to write html, head, body tags, it is handled by the widget -->\n<div id=\"root\"></div>\n",
2828
"css": ".app {\n\theight: calc(var(--appsmith-ui-height) * 1px);\n\twidth: calc(var(--appsmith-ui-width) * 1px);\n\tjustify-content: center;\n\tborder-radius: var(--appsmith-theme-borderRadius);\n\tbox-shadow: var(--appsmith-theme-boxShadow);\n}\n\n.tip-container {\n margin-bottom: 20px;\n}\n\n.tip-container h2 {\n margin-bottom: 20px;\n\tfont-size: 16px;\n\tfont-weight: 700;\n}\n\n.tip-header {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: baseline;\n}\n\n.tip-header div {\n\tcolor: #999;\n}\n\n.button-container {\n\ttext-align: right;\t\n}\n\n.button-container button {\n margin: 0 10px;\n}\n\n.button-container button.primary {\n\tbackground: var(--appsmith-theme-primaryColor) !important;\n}\n\n.button-container button.reset {\n\tcolor: var(--appsmith-theme-primaryColor) !important;\n\tborder-color: var(--appsmith-theme-primaryColor) !important;\n}",
29-
"js": "import React from 'https://cdn.jsdelivr.net/npm/react@18.2.0/+esm';\nimport reactDom from 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm';\nimport { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.11.1/+esm';\nimport Markdown from 'https://cdn.jsdelivr.net/npm/react-markdown@9.0.1/+esm';\nfunction App() {\n const [currentIndex, setCurrentIndex] = React.useState(0);\n const [key, setKey] = React.useState(0);\n const handleNext = () => {\n const index = (currentIndex + 1) % appsmith.model.tips.length;\n setCurrentIndex(index);\n appsmith.updateModel({\n currentIndex: index\n });\n };\n const handleReset = () => {\n setCurrentIndex(0);\n appsmith.updateModel({\n currentIndex: 0\n });\n appsmith.triggerEvent(\"onResetClick\", {\n oldIndex: currentIndex\n });\n };\n React.useEffect(() => {\n appsmith.onModelChange((model, prevModel) => {\n if (JSON.stringify(prevModel?.tips) !== JSON.stringify(model.tips)) {\n setKey(Math.random());\n }\n });\n }, []);\n return /*#__PURE__*/React.createElement(Card, {\n className: \"app\",\n key: key\n }, /*#__PURE__*/React.createElement(\"div\", {\n className: \"tip-container\"\n }, /*#__PURE__*/React.createElement(\"div\", {\n className: \"tip-header\"\n }, /*#__PURE__*/React.createElement(\"h2\", null, \"Custom Widget\"), /*#__PURE__*/React.createElement(\"div\", null, currentIndex + 1, \" / \", appsmith.model.tips.length, \" \")), /*#__PURE__*/React.createElement(Markdown, null, appsmith.model.tips[currentIndex])), /*#__PURE__*/React.createElement(\"div\", {\n className: \"button-container\"\n }, /*#__PURE__*/React.createElement(Button, {\n className: \"primary\",\n onClick: handleNext,\n type: \"primary\"\n }, \"Next Tip\"), /*#__PURE__*/React.createElement(Button, {\n className: \"reset\",\n onClick: handleReset\n }, \"Reset\")));\n}\nappsmith.onReady(() => {\n reactDom.render( /*#__PURE__*/React.createElement(App, null), document.getElementById(\"root\"));\n});"
29+
"js": "import React from 'https://cdn.jsdelivr.net/npm/react@18.2.0/+esm';\nimport reactDom from 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm';\nimport { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.15.0/+esm';\nimport Markdown from 'https://cdn.jsdelivr.net/npm/react-markdown@9.0.1/+esm';\nfunction App() {\n const [currentIndex, setCurrentIndex] = React.useState(0);\n const [key, setKey] = React.useState(0);\n const handleNext = () => {\n const index = (currentIndex + 1) % appsmith.model.tips.length;\n setCurrentIndex(index);\n appsmith.updateModel({\n currentIndex: index\n });\n };\n const handleReset = () => {\n setCurrentIndex(0);\n appsmith.updateModel({\n currentIndex: 0\n });\n appsmith.triggerEvent(\"onResetClick\", {\n oldIndex: currentIndex\n });\n };\n React.useEffect(() => {\n appsmith.onModelChange((model, prevModel) => {\n if (JSON.stringify(prevModel?.tips) !== JSON.stringify(model.tips)) {\n setKey(Math.random());\n }\n });\n }, []);\n return /*#__PURE__*/React.createElement(Card, {\n className: \"app\",\n key: key\n }, /*#__PURE__*/React.createElement(\"div\", {\n className: \"tip-container\"\n }, /*#__PURE__*/React.createElement(\"div\", {\n className: \"tip-header\"\n }, /*#__PURE__*/React.createElement(\"h2\", null, \"Custom Widget\"), /*#__PURE__*/React.createElement(\"div\", null, currentIndex + 1, \" / \", appsmith.model.tips.length, \" \")), /*#__PURE__*/React.createElement(Markdown, null, appsmith.model.tips[currentIndex])), /*#__PURE__*/React.createElement(\"div\", {\n className: \"button-container\"\n }, /*#__PURE__*/React.createElement(Button, {\n className: \"primary\",\n onClick: handleNext,\n type: \"primary\"\n }, \"Next Tip\"), /*#__PURE__*/React.createElement(Button, {\n className: \"reset\",\n onClick: handleReset\n }, \"Reset\")));\n}\nappsmith.onReady(() => {\n reactDom.render( /*#__PURE__*/React.createElement(App, null), document.getElementById(\"root\"));\n});"
3030
},
3131
"isCanvas": false,
3232
"displayName": "Custom",
@@ -68,7 +68,7 @@
6868
"uncompiledSrcDoc": {
6969
"html": "<!-- no need to write html, head, body tags, it is handled by the widget -->\n<div id=\"root\"></div>\n",
7070
"css": ".app {\n\theight: calc(var(--appsmith-ui-height) * 1px);\n\twidth: calc(var(--appsmith-ui-width) * 1px);\n\tjustify-content: center;\n\tborder-radius: var(--appsmith-theme-borderRadius);\n\tbox-shadow: var(--appsmith-theme-boxShadow);\n}\n\n.tip-container {\n margin-bottom: 20px;\n}\n\n.tip-container h2 {\n margin-bottom: 20px;\n\tfont-size: 16px;\n\tfont-weight: 700;\n}\n\n.tip-header {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: baseline;\n}\n\n.tip-header div {\n\tcolor: #999;\n}\n\n.button-container {\n\ttext-align: right;\t\n}\n\n.button-container button {\n margin: 0 10px;\n}\n\n.button-container button.primary {\n\tbackground: var(--appsmith-theme-primaryColor) !important;\n}\n\n.button-container button.reset {\n\tcolor: var(--appsmith-theme-primaryColor) !important;\n\tborder-color: var(--appsmith-theme-primaryColor) !important;\n}",
71-
"js": "import React from 'https://cdn.jsdelivr.net/npm/react@18.2.0/+esm'\nimport reactDom from 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm'\nimport { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.11.1/+esm'\nimport Markdown from 'https://cdn.jsdelivr.net/npm/react-markdown@9.0.1/+esm';\n\nfunction App() {\n\tconst [currentIndex, setCurrentIndex] = React.useState(0);\n\t\n\tconst [key, setKey] = React.useState(0);\n\n\tconst handleNext = () => {\n\t\tconst index = (currentIndex + 1) % appsmith.model.tips.length\n\t\tsetCurrentIndex(index);\n\t\tappsmith.updateModel({\n\t\t\tcurrentIndex: index\n\t\t});\n\t};\n\n\tconst handleReset = () => {\n\t\tsetCurrentIndex(0);\n\t\tappsmith.updateModel({\n\t\t\tcurrentIndex: 0\n\t\t});\n\t\tappsmith.triggerEvent(\"onResetClick\", {\n\t\t\toldIndex: currentIndex\n\t\t});\n\t};\n\t\n\tReact.useEffect(() => {\t\t\n\t\tappsmith.onModelChange((model, prevModel) => {\n\t\t\tif (JSON.stringify(prevModel?.tips) !== JSON.stringify(model.tips)) {\n\t\t\t\tsetKey(Math.random());\n\t\t\t}\n\t\t});\n\t}, []);\n\t\n\n\treturn (\n\t\t<Card className=\"app\" key={key}>\n\t\t\t<div className=\"tip-container\">\n\t\t\t\t<div className=\"tip-header\">\n\t\t\t\t\t<h2>Custom Widget</h2>\n\t\t\t\t\t<div>{currentIndex + 1} / {appsmith.model.tips.length}\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<Markdown>{appsmith.model.tips[currentIndex]}</Markdown>\n\t\t\t</div>\n\t\t\t<div className=\"button-container\">\n\t\t\t\t<Button className=\"primary\" onClick={handleNext} type=\"primary\">Next Tip</Button>\n\t\t\t\t<Button className=\"reset\" onClick={handleReset}>Reset</Button>\n\t\t\t</div>\n\t</Card>\n);\n}\n\nappsmith.onReady(() => {\n\treactDom.render(<App />, document.getElementById(\"root\"));\n});"
71+
"js": "import React from 'https://cdn.jsdelivr.net/npm/react@18.2.0/+esm'\nimport reactDom from 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm'\nimport { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.15.0/+esm'\nimport Markdown from 'https://cdn.jsdelivr.net/npm/react-markdown@9.0.1/+esm';\n\nfunction App() {\n\tconst [currentIndex, setCurrentIndex] = React.useState(0);\n\t\n\tconst [key, setKey] = React.useState(0);\n\n\tconst handleNext = () => {\n\t\tconst index = (currentIndex + 1) % appsmith.model.tips.length\n\t\tsetCurrentIndex(index);\n\t\tappsmith.updateModel({\n\t\t\tcurrentIndex: index\n\t\t});\n\t};\n\n\tconst handleReset = () => {\n\t\tsetCurrentIndex(0);\n\t\tappsmith.updateModel({\n\t\t\tcurrentIndex: 0\n\t\t});\n\t\tappsmith.triggerEvent(\"onResetClick\", {\n\t\t\toldIndex: currentIndex\n\t\t});\n\t};\n\t\n\tReact.useEffect(() => {\t\t\n\t\tappsmith.onModelChange((model, prevModel) => {\n\t\t\tif (JSON.stringify(prevModel?.tips) !== JSON.stringify(model.tips)) {\n\t\t\t\tsetKey(Math.random());\n\t\t\t}\n\t\t});\n\t}, []);\n\t\n\n\treturn (\n\t\t<Card className=\"app\" key={key}>\n\t\t\t<div className=\"tip-container\">\n\t\t\t\t<div className=\"tip-header\">\n\t\t\t\t\t<h2>Custom Widget</h2>\n\t\t\t\t\t<div>{currentIndex + 1} / {appsmith.model.tips.length}\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<Markdown>{appsmith.model.tips[currentIndex]}</Markdown>\n\t\t\t</div>\n\t\t\t<div className=\"button-container\">\n\t\t\t\t<Button className=\"primary\" onClick={handleNext} type=\"primary\">Next Tip</Button>\n\t\t\t\t<Button className=\"reset\" onClick={handleReset}>Reset</Button>\n\t\t\t</div>\n\t</Card>\n);\n}\n\nappsmith.onReady(() => {\n\treactDom.render(<App />, document.getElementById(\"root\"));\n});"
7272
},
7373
"parentId": "0",
7474
"tags": [

app/client/src/ce/reducers/entityReducers/canvasWidgetsReducer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export type FlattenedWidgetProps<orType = never> =
3939
* @param updateLayoutDiff
4040
* @returns list of widgets that were updated
4141
*/
42-
function getUpdatedWidgetLists(
42+
export function getUpdatedWidgetLists(
4343
updateLayoutDiff: Diff<
4444
CanvasWidgetsReduxState,
4545
{

app/client/src/ee/RouteParamsMiddleware.ts

-3
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { Middleware } from "redux";
2+
3+
// This middleware is extended in EE to add package specific logic
4+
const PackageMiddleware: Middleware = () => (next) => (action) => {
5+
// Simply pass the action to the next middleware/reducer
6+
return next(action);
7+
};
8+
9+
export default PackageMiddleware;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "ce/middlewares/RouteParamsMiddleware";
2+
import { default as EE_RouteParamsMiddleware } from "ce/middlewares/RouteParamsMiddleware";
3+
export default EE_RouteParamsMiddleware;

app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/anvilTemplates/react.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export default {
5252
}`,
5353
js: `import React from 'https://cdn.jsdelivr.net/npm/react@18.2.0/+esm'
5454
import reactDom from 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm'
55-
import { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.11.1/+esm'
55+
import { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.15.0/+esm'
5656
import Markdown from 'https://cdn.jsdelivr.net/npm/react-markdown@9.0.1/+esm'
5757
5858
function App() {

app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/react.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export default {
5454
}`,
5555
js: `import React from 'https://cdn.jsdelivr.net/npm/react@18.2.0/+esm'
5656
import reactDom from 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm'
57-
import { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.11.1/+esm'
57+
import { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.15.0/+esm'
5858
import Markdown from 'https://cdn.jsdelivr.net/npm/react-markdown@9.0.1/+esm'
5959
6060
function App() {

app/client/src/store.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { rootSaga } from "ee/sagas";
77
import { composeWithDevTools } from "redux-devtools-extension/logOnlyInProduction";
88
import * as Sentry from "@sentry/react";
99
import { ReduxActionTypes } from "ee/constants/ReduxActionConstants";
10-
import routeParamsMiddleware from "ee/RouteParamsMiddleware";
10+
import routeParamsMiddleware from "ee/middlewares/RouteParamsMiddleware";
1111

1212
const sagaMiddleware = createSagaMiddleware();
1313
const ignoredSentryActionTypes = [

app/client/src/widgets/CustomWidget/widget/defaultApp.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export default {
5252
`,
5353
js: `import React from 'https://cdn.jsdelivr.net/npm/react@18.2.0/+esm'
5454
import reactDom from 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm'
55-
import { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.11.1/+esm'
55+
import { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.15.0/+esm'
5656
import Markdown from 'https://cdn.jsdelivr.net/npm/react-markdown@9.0.1/+esm';
5757
5858
function App() {
@@ -143,7 +143,7 @@ appsmith.onReady(() => {
143143
}`,
144144
js: `import React from 'https://cdn.jsdelivr.net/npm/react@18.2.0/+esm';
145145
import reactDom from 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm';
146-
import { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.11.1/+esm';
146+
import { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.15.0/+esm';
147147
import Markdown from 'https://cdn.jsdelivr.net/npm/react-markdown@9.0.1/+esm';
148148
149149
function App() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import React from "react";
2+
import { render } from "@testing-library/react";
3+
import Marker from "../component/Marker";
4+
5+
// Mock the google maps API
6+
const mockAddListener = jest.fn().mockImplementation((event, callback) => {
7+
// Store the callback to simulate click events
8+
if (event === "click") {
9+
(
10+
mockAddListener as unknown as {
11+
clickCallback: (...args: unknown[]) => void;
12+
}
13+
).clickCallback = callback;
14+
}
15+
16+
return "listener-id"; // Return a mock listener ID
17+
});
18+
19+
const mockRemoveListener = jest.fn();
20+
const mockSetMap = jest.fn();
21+
const mockSetIcon = jest.fn();
22+
const mockSetPosition = jest.fn();
23+
const mockSetTitle = jest.fn();
24+
25+
// Add type declaration for the global google object
26+
declare global {
27+
interface Window {
28+
google: unknown;
29+
}
30+
}
31+
32+
// Mock the google object
33+
(global as unknown as { google: unknown }).google = {
34+
maps: {
35+
Marker: jest.fn().mockImplementation(() => ({
36+
setMap: mockSetMap,
37+
setIcon: mockSetIcon,
38+
setPosition: mockSetPosition,
39+
setTitle: mockSetTitle,
40+
addListener: mockAddListener,
41+
})),
42+
Point: jest.fn().mockImplementation((x, y) => ({ x, y })),
43+
event: {
44+
clearListeners: jest.fn(),
45+
removeListener: mockRemoveListener,
46+
},
47+
},
48+
};
49+
50+
describe("Map Widget - Marker Component", () => {
51+
beforeEach(() => {
52+
jest.clearAllMocks();
53+
});
54+
55+
it("should trigger onClick callback only once per click", () => {
56+
const onClickMock = jest.fn();
57+
58+
// Render the marker component with onClick handler
59+
render(
60+
<Marker
61+
onClick={onClickMock}
62+
position={{ lat: 37.7749, lng: -122.4194 }}
63+
title="Test Marker"
64+
/>,
65+
);
66+
67+
// Simulate a marker click by directly calling the stored callback
68+
(
69+
mockAddListener as unknown as {
70+
clickCallback: (...args: unknown[]) => void;
71+
}
72+
).clickCallback();
73+
74+
// Verify onClick was called exactly once
75+
expect(onClickMock).toHaveBeenCalledTimes(1);
76+
77+
// Simulate another click
78+
(
79+
mockAddListener as unknown as {
80+
clickCallback: (...args: unknown[]) => void;
81+
}
82+
).clickCallback();
83+
84+
// Verify onClick was called exactly twice (once per click)
85+
expect(onClickMock).toHaveBeenCalledTimes(2);
86+
});
87+
88+
it("should clear previous click listeners when onClick prop changes", () => {
89+
const { rerender } = render(
90+
<Marker
91+
onClick={jest.fn()}
92+
position={{ lat: 37.7749, lng: -122.4194 }}
93+
title="Test Marker"
94+
/>,
95+
);
96+
97+
// Verify clearListeners was called during initial render
98+
expect(google.maps.event.clearListeners).toHaveBeenCalledWith(
99+
expect.anything(),
100+
"click",
101+
);
102+
103+
// Reset the mock to check if it's called again
104+
(google.maps.event.clearListeners as jest.Mock).mockClear();
105+
106+
// Rerender with a different onClick handler
107+
rerender(
108+
<Marker
109+
onClick={jest.fn()}
110+
position={{ lat: 37.7749, lng: -122.4194 }}
111+
title="Test Marker"
112+
/>,
113+
);
114+
115+
// Verify clearListeners was called again when onClick changed
116+
expect(google.maps.event.clearListeners).toHaveBeenCalledWith(
117+
expect.anything(),
118+
"click",
119+
);
120+
});
121+
});

app/client/src/widgets/MapWidget/component/Marker.tsx

+19-10
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,6 @@ const Marker: React.FC<MarkerProps> = (options) => {
3131
title,
3232
});
3333

34-
googleMapMarker.addListener("click", () => {
35-
if (onClick) onClick();
36-
});
37-
3834
setMarker(googleMapMarker);
3935
}
4036

@@ -79,19 +75,32 @@ const Marker: React.FC<MarkerProps> = (options) => {
7975
useEffect(() => {
8076
if (!marker) return;
8177

82-
marker.addListener("click", () => {
78+
google.maps.event.clearListeners(marker, "click");
79+
const clickListener = marker.addListener("click", () => {
8380
if (onClick) onClick();
8481
});
82+
83+
return () => {
84+
google.maps.event.removeListener(clickListener);
85+
};
8586
}, [marker, onClick]);
8687

8788
// add dragend event on marker
8889
useEffect(() => {
89-
if (!marker) return;
90+
if (!marker || !onDragEnd) return;
9091

91-
marker.addListener("dragend", (e: google.maps.MapMouseEvent) => {
92-
if (onDragEnd) onDragEnd(e);
93-
});
94-
}, [marker, options.onDragEnd]);
92+
google.maps.event.clearListeners(marker, "dragend");
93+
const dragEndListener = marker.addListener(
94+
"dragend",
95+
(e: google.maps.MapMouseEvent) => {
96+
if (onDragEnd) onDragEnd(e);
97+
},
98+
);
99+
100+
return () => {
101+
google.maps.event.removeListener(dragEndListener);
102+
};
103+
}, [marker, onDragEnd]);
95104

96105
return null;
97106
};

0 commit comments

Comments
 (0)