Skip to content

Commit 8fc2380

Browse files
authored
[WC-3477] Do not render image when the image is not available (#2298)
2 parents bb09f79 + 69d0e36 commit 8fc2380

6 files changed

Lines changed: 272 additions & 81 deletions

File tree

packages/pluggableWidgets/image-web/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1010

1111
- We fixed an issue where previews in Design and Structure modes did not respect the configured minimum and maximum image height.
1212

13+
- We fixed an issue where the Image widget would briefly render in an empty or broken state while the image data was still loading. The widget now waits until the image is ready before displaying it.
14+
1315
## [1.5.1] - 2025-10-29
1416

1517
### Fixed

packages/pluggableWidgets/image-web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"@mendix/prettier-config-web-widgets": "workspace:*",
5555
"@mendix/rollup-web-widgets": "workspace:*",
5656
"@mendix/run-e2e": "workspace:*",
57-
"@mendix/widget-plugin-platform": "workspace:*"
57+
"@mendix/widget-plugin-platform": "workspace:*",
58+
"@mendix/widget-plugin-test-utils": "workspace:*"
5859
}
5960
}
Lines changed: 4 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,9 @@
11
import { ValueStatus } from "mendix";
22
import { FunctionComponent, useCallback } from "react";
33
import { ImageContainerProps } from "../typings/ImageProps";
4-
import { Image as ImageComponent, ImageType } from "./components/Image/Image";
4+
import { Image as ImageComponent } from "./components/Image/Image";
55
import { constructStyleObject } from "./utils/helpers";
6-
7-
function getImageProps({
8-
datasource,
9-
imageIcon,
10-
imageObject,
11-
imageUrl,
12-
defaultImageDynamic
13-
}: ImageContainerProps): ImageType {
14-
const fallback: ImageType = {
15-
type: "image",
16-
image: undefined
17-
};
18-
switch (datasource) {
19-
case "image": {
20-
if (imageObject?.status === ValueStatus.Available) {
21-
return {
22-
type: "image",
23-
image: imageObject.value.uri
24-
};
25-
} else if (
26-
imageObject?.status === ValueStatus.Unavailable &&
27-
defaultImageDynamic?.status === ValueStatus.Available
28-
) {
29-
return {
30-
type: "image",
31-
image: defaultImageDynamic.value.uri
32-
};
33-
}
34-
return {
35-
type: "image",
36-
image: undefined
37-
};
38-
}
39-
case "imageUrl":
40-
return {
41-
type: "image",
42-
image: imageUrl?.status === ValueStatus.Available ? imageUrl.value : undefined
43-
};
44-
case "icon": {
45-
if (imageIcon?.status === ValueStatus.Available && imageIcon.value) {
46-
const icon = imageIcon.value;
47-
return {
48-
type: icon.type,
49-
image: icon.type === "image" ? icon.iconUrl : icon.iconClass
50-
};
51-
}
52-
return fallback;
53-
}
54-
default:
55-
return fallback;
56-
}
57-
}
6+
import { getImageProps } from "./utils/getImageProps";
587

598
export const Image: FunctionComponent<ImageContainerProps> = props => {
609
const onClick = useCallback(() => props.onClick?.execute(), [props.onClick]);
@@ -65,7 +14,7 @@ export const Image: FunctionComponent<ImageContainerProps> = props => {
6514

6615
const imageStyle = { ...props.style, ...styleObject };
6716

68-
return (
17+
return image ? (
6918
<ImageComponent
7019
class={props.class}
7120
style={imageStyle}
@@ -85,5 +34,5 @@ export const Image: FunctionComponent<ImageContainerProps> = props => {
8534
renderAsBackground={props.datasource !== "icon" && props.isBackgroundImage}
8635
backgroundImageContent={props.children}
8736
/>
88-
);
37+
) : null;
8938
};
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { WebIcon, WebImage } from "mendix";
2+
import { dynamic } from "@mendix/widget-plugin-test-utils";
3+
import { getImageProps, GetImagePropsInput } from "../getImageProps";
4+
5+
const webImage = (uri: string): WebImage => ({ uri, name: "test.jpg" });
6+
7+
describe("getImageProps", () => {
8+
describe('datasource: "image"', () => {
9+
it("returns the main image URI when main image is available", () => {
10+
const input: GetImagePropsInput = {
11+
datasource: "image",
12+
imageObject: dynamic.available(webImage("https://example.com/main.jpg"))
13+
};
14+
expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/main.jpg" });
15+
});
16+
17+
it("returns the main image URI when main image is loading (uri present)", () => {
18+
const input: GetImagePropsInput = {
19+
datasource: "image",
20+
imageObject: dynamic.loading(webImage("https://example.com/main.jpg"))
21+
};
22+
expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/main.jpg" });
23+
});
24+
25+
it("returns undefined image when main image is loading (no uri yet)", () => {
26+
const input: GetImagePropsInput = {
27+
datasource: "image",
28+
imageObject: dynamic.loading<WebImage>()
29+
};
30+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
31+
});
32+
33+
it("falls back to defaultImage when main image is unavailable and fallback is available", () => {
34+
const input: GetImagePropsInput = {
35+
datasource: "image",
36+
imageObject: dynamic.unavailable<WebImage>(),
37+
defaultImageDynamic: dynamic.available(webImage("https://example.com/fallback.jpg"))
38+
};
39+
expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/fallback.jpg" });
40+
});
41+
42+
it("falls back to defaultImage when main image is unavailable and fallback is loading", () => {
43+
const input: GetImagePropsInput = {
44+
datasource: "image",
45+
imageObject: dynamic.unavailable<WebImage>(),
46+
defaultImageDynamic: dynamic.loading(webImage("https://example.com/fallback.jpg"))
47+
};
48+
expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/fallback.jpg" });
49+
});
50+
51+
it("returns undefined image when both main image and fallback are unavailable", () => {
52+
const input: GetImagePropsInput = {
53+
datasource: "image",
54+
imageObject: dynamic.unavailable<WebImage>(),
55+
defaultImageDynamic: dynamic.unavailable<WebImage>()
56+
};
57+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
58+
});
59+
60+
it("returns undefined image when imageObject and defaultImageDynamic are not provided", () => {
61+
const input: GetImagePropsInput = { datasource: "image" };
62+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
63+
});
64+
65+
it("does not use defaultImage when main image is available", () => {
66+
const input: GetImagePropsInput = {
67+
datasource: "image",
68+
imageObject: dynamic.available(webImage("https://example.com/main.jpg")),
69+
defaultImageDynamic: dynamic.available(webImage("https://example.com/fallback.jpg"))
70+
};
71+
expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/main.jpg" });
72+
});
73+
});
74+
75+
describe('datasource: "imageUrl"', () => {
76+
it("returns the URL when imageUrl is available", () => {
77+
const input: GetImagePropsInput = {
78+
datasource: "imageUrl",
79+
imageUrl: dynamic.available("https://example.com/image.jpg")
80+
};
81+
expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/image.jpg" });
82+
});
83+
84+
it("returns stale URL when imageUrl is loading with a cached value", () => {
85+
const input: GetImagePropsInput = {
86+
datasource: "imageUrl",
87+
imageUrl: dynamic.loading("https://example.com/image.jpg")
88+
};
89+
expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/image.jpg" });
90+
});
91+
92+
it("returns undefined image when imageUrl is loading with no cached value", () => {
93+
const input: GetImagePropsInput = {
94+
datasource: "imageUrl",
95+
imageUrl: dynamic.loading<string>()
96+
};
97+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
98+
});
99+
100+
it("returns undefined image when imageUrl is unavailable", () => {
101+
const input: GetImagePropsInput = {
102+
datasource: "imageUrl",
103+
imageUrl: dynamic.unavailable<string>()
104+
};
105+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
106+
});
107+
108+
it("returns undefined image when imageUrl is not provided", () => {
109+
const input: GetImagePropsInput = { datasource: "imageUrl" };
110+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
111+
});
112+
});
113+
114+
describe('datasource: "icon"', () => {
115+
it("returns glyph icon class when a glyph icon is available", () => {
116+
const glyphIcon: WebIcon = { type: "glyph", iconClass: "glyphicon-star" };
117+
const input: GetImagePropsInput = {
118+
datasource: "icon",
119+
imageIcon: dynamic.available(glyphIcon)
120+
};
121+
expect(getImageProps(input)).toEqual({ type: "glyph", image: "glyphicon-star" });
122+
});
123+
124+
it("returns image icon URL when an image icon is available", () => {
125+
const imageIcon: WebIcon = { type: "image", iconUrl: "https://example.com/icon.png" };
126+
const input: GetImagePropsInput = {
127+
datasource: "icon",
128+
imageIcon: dynamic.available(imageIcon)
129+
};
130+
expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/icon.png" });
131+
});
132+
133+
it("returns mx-icon class when a named icon is available", () => {
134+
const namedIcon: WebIcon = { type: "icon", iconClass: "mx-icon-star" };
135+
const input: GetImagePropsInput = {
136+
datasource: "icon",
137+
imageIcon: dynamic.available(namedIcon)
138+
};
139+
expect(getImageProps(input)).toEqual({ type: "icon", image: "mx-icon-star" });
140+
});
141+
142+
it("returns fallback when imageIcon is loading", () => {
143+
const input: GetImagePropsInput = {
144+
datasource: "icon",
145+
imageIcon: dynamic.loading<WebIcon>()
146+
};
147+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
148+
});
149+
150+
it("returns fallback when imageIcon is unavailable", () => {
151+
const input: GetImagePropsInput = {
152+
datasource: "icon",
153+
imageIcon: dynamic.unavailable<WebIcon>()
154+
};
155+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
156+
});
157+
158+
it("returns fallback when imageIcon value is undefined (WebIcon = undefined)", () => {
159+
const input: GetImagePropsInput = {
160+
datasource: "icon",
161+
imageIcon: dynamic.available(undefined as WebIcon)
162+
};
163+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
164+
});
165+
166+
it("returns fallback when imageIcon is not provided", () => {
167+
const input: GetImagePropsInput = { datasource: "icon" };
168+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
169+
});
170+
});
171+
172+
describe("unknown datasource", () => {
173+
it("returns fallback for an unrecognised datasource value", () => {
174+
const input = { datasource: "unknown" as GetImagePropsInput["datasource"] };
175+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
176+
});
177+
});
178+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { ValueStatus } from "mendix";
2+
import { ImageContainerProps } from "../../typings/ImageProps";
3+
import { ImageType } from "../components/Image/Image";
4+
5+
export type GetImagePropsInput = Pick<
6+
ImageContainerProps,
7+
"datasource" | "imageIcon" | "imageObject" | "imageUrl" | "defaultImageDynamic"
8+
>;
9+
10+
const fallback: ImageType = { type: "image", image: undefined };
11+
12+
export function getImageProps({
13+
datasource,
14+
imageIcon,
15+
imageObject,
16+
imageUrl,
17+
defaultImageDynamic: defaultImage
18+
}: GetImagePropsInput): ImageType {
19+
switch (datasource) {
20+
case "image": {
21+
if (imageObject?.status === ValueStatus.Available || imageObject?.status === ValueStatus.Loading) {
22+
return {
23+
type: "image",
24+
image: imageObject?.value?.uri
25+
};
26+
}
27+
28+
if (defaultImage?.status === ValueStatus.Available || defaultImage?.status === ValueStatus.Loading) {
29+
return {
30+
type: "image",
31+
image: defaultImage?.value?.uri
32+
};
33+
}
34+
35+
return { type: "image", image: undefined };
36+
}
37+
case "imageUrl":
38+
return {
39+
type: "image",
40+
image:
41+
imageUrl?.status === ValueStatus.Available || imageUrl?.status === ValueStatus.Loading
42+
? imageUrl.value
43+
: undefined
44+
};
45+
case "icon": {
46+
if (imageIcon?.status === ValueStatus.Available && imageIcon.value) {
47+
const icon = imageIcon.value;
48+
return {
49+
type: icon.type,
50+
image: icon.type === "image" ? icon.iconUrl : icon.iconClass
51+
};
52+
}
53+
return fallback;
54+
}
55+
default:
56+
return fallback;
57+
}
58+
}

0 commit comments

Comments
 (0)