Skip to content

Commit 5f98ebe

Browse files
committed
fix: do not render image when the image is not available
1 parent d376dbf commit 5f98ebe

6 files changed

Lines changed: 243 additions & 59 deletions

File tree

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

packages/pluggableWidgets/image-web/src/components/__tests__/__snapshots__/Image.spec.tsx.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ exports[`Image renders the structure as a background image 1`] = `
44
<DocumentFragment>
55
<div
66
class="mx-image-viewer mx-image-background"
7-
style="height: 300px; width: 300px; background-image: url(https://pbs.twimg.com/profile_images/1905729715/llamas_1_.jpg);"
7+
style="height: 300px; width: 300px; background-image: url("https://pbs.twimg.com/profile_images/1905729715/llamas_1_.jpg");"
88
>
99
<div>
1010
Image content
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { DynamicValue, ValueStatus, 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+
const loadingDynamic = <T>(): DynamicValue<T> =>
8+
({ status: "loading" as ValueStatus.Loading, value: undefined }) as DynamicValue<T>;
9+
10+
describe("getImageProps", () => {
11+
describe('datasource: "image"', () => {
12+
it("returns the main image URI when main image is available", () => {
13+
const input: GetImagePropsInput = {
14+
datasource: "image",
15+
imageObject: dynamic(webImage("https://example.com/main.jpg"))
16+
};
17+
expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/main.jpg" });
18+
});
19+
20+
it("returns the main image URI when main image is loading (uri present)", () => {
21+
const input: GetImagePropsInput = {
22+
datasource: "image",
23+
imageObject: dynamic(webImage("https://example.com/main.jpg"), true)
24+
};
25+
expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/main.jpg" });
26+
});
27+
28+
it("returns undefined image when main image is loading (no uri yet)", () => {
29+
const input: GetImagePropsInput = {
30+
datasource: "image",
31+
imageObject: loadingDynamic<WebImage>()
32+
};
33+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
34+
});
35+
36+
it("falls back to defaultImage when main image is unavailable and fallback is available", () => {
37+
const input: GetImagePropsInput = {
38+
datasource: "image",
39+
imageObject: dynamic<WebImage>(),
40+
defaultImageDynamic: dynamic(webImage("https://example.com/fallback.jpg"))
41+
};
42+
expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/fallback.jpg" });
43+
});
44+
45+
it("falls back to defaultImage when main image is unavailable and fallback is loading", () => {
46+
const input: GetImagePropsInput = {
47+
datasource: "image",
48+
imageObject: dynamic<WebImage>(),
49+
defaultImageDynamic: dynamic(webImage("https://example.com/fallback.jpg"), true)
50+
};
51+
expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/fallback.jpg" });
52+
});
53+
54+
it("returns undefined image when both main image and fallback are unavailable", () => {
55+
const input: GetImagePropsInput = {
56+
datasource: "image",
57+
imageObject: dynamic<WebImage>(),
58+
defaultImageDynamic: dynamic<WebImage>()
59+
};
60+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
61+
});
62+
63+
it("returns undefined image when imageObject and defaultImageDynamic are not provided", () => {
64+
const input: GetImagePropsInput = { datasource: "image" };
65+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
66+
});
67+
68+
it("does not use defaultImage when main image is available", () => {
69+
const input: GetImagePropsInput = {
70+
datasource: "image",
71+
imageObject: dynamic(webImage("https://example.com/main.jpg")),
72+
defaultImageDynamic: dynamic(webImage("https://example.com/fallback.jpg"))
73+
};
74+
expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/main.jpg" });
75+
});
76+
});
77+
78+
describe('datasource: "imageUrl"', () => {
79+
it("returns the URL when imageUrl is available", () => {
80+
const input: GetImagePropsInput = {
81+
datasource: "imageUrl",
82+
imageUrl: dynamic("https://example.com/image.jpg")
83+
};
84+
expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/image.jpg" });
85+
});
86+
87+
it("returns undefined image when imageUrl is loading", () => {
88+
const input: GetImagePropsInput = {
89+
datasource: "imageUrl",
90+
imageUrl: loadingDynamic<string>()
91+
};
92+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
93+
});
94+
95+
it("returns undefined image when imageUrl is unavailable", () => {
96+
const input: GetImagePropsInput = {
97+
datasource: "imageUrl",
98+
imageUrl: dynamic<string>()
99+
};
100+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
101+
});
102+
103+
it("returns undefined image when imageUrl is not provided", () => {
104+
const input: GetImagePropsInput = { datasource: "imageUrl" };
105+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
106+
});
107+
});
108+
109+
describe('datasource: "icon"', () => {
110+
it("returns glyph icon class when a glyph icon is available", () => {
111+
const glyphIcon: WebIcon = { type: "glyph", iconClass: "glyphicon-star" };
112+
const input: GetImagePropsInput = {
113+
datasource: "icon",
114+
imageIcon: dynamic(glyphIcon)
115+
};
116+
expect(getImageProps(input)).toEqual({ type: "glyph", image: "glyphicon-star" });
117+
});
118+
119+
it("returns image icon URL when an image icon is available", () => {
120+
const imageIcon: WebIcon = { type: "image", iconUrl: "https://example.com/icon.png" };
121+
const input: GetImagePropsInput = {
122+
datasource: "icon",
123+
imageIcon: dynamic(imageIcon)
124+
};
125+
expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/icon.png" });
126+
});
127+
128+
it("returns mx-icon class when a named icon is available", () => {
129+
const namedIcon: WebIcon = { type: "icon", iconClass: "mx-icon-star" };
130+
const input: GetImagePropsInput = {
131+
datasource: "icon",
132+
imageIcon: dynamic(namedIcon)
133+
};
134+
expect(getImageProps(input)).toEqual({ type: "icon", image: "mx-icon-star" });
135+
});
136+
137+
it("returns fallback when imageIcon is loading", () => {
138+
const input: GetImagePropsInput = {
139+
datasource: "icon",
140+
imageIcon: loadingDynamic<WebIcon>()
141+
};
142+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
143+
});
144+
145+
it("returns fallback when imageIcon is unavailable", () => {
146+
const input: GetImagePropsInput = {
147+
datasource: "icon",
148+
imageIcon: dynamic<WebIcon>()
149+
};
150+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
151+
});
152+
153+
it("returns fallback when imageIcon value is undefined (WebIcon = undefined)", () => {
154+
const input: GetImagePropsInput = {
155+
datasource: "icon",
156+
imageIcon: dynamic<WebIcon>(undefined as WebIcon)
157+
};
158+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
159+
});
160+
161+
it("returns fallback when imageIcon is not provided", () => {
162+
const input: GetImagePropsInput = { datasource: "icon" };
163+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
164+
});
165+
});
166+
167+
describe("unknown datasource", () => {
168+
it("returns fallback for an unrecognised datasource value", () => {
169+
const input = { datasource: "unknown" as GetImagePropsInput["datasource"] };
170+
expect(getImageProps(input)).toEqual({ type: "image", image: undefined });
171+
});
172+
});
173+
});
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 main image is available or loading
22+
if (imageObject?.status === ValueStatus.Available || imageObject?.status === ValueStatus.Loading) {
23+
return {
24+
type: "image",
25+
image: imageObject?.value?.uri
26+
};
27+
}
28+
29+
// if main image is not available, but fallback is available or loading
30+
if (defaultImage?.status === ValueStatus.Available || defaultImage?.status === ValueStatus.Loading) {
31+
return {
32+
type: "image",
33+
image: defaultImage?.value?.uri
34+
};
35+
}
36+
37+
// if main image and fallback are not available
38+
return { type: "image", image: undefined };
39+
}
40+
case "imageUrl":
41+
return {
42+
type: "image",
43+
image: imageUrl?.status === ValueStatus.Available ? imageUrl.value : 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+
}

pnpm-lock.yaml

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)