From a1f23f453867e04bc1c22d83673a335ae195355d Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Thu, 23 Jul 2026 15:47:07 +0200 Subject: [PATCH 1/7] fix(image-web): migrate image properties to allowUpload, fix deprecation warning --- .../.openspec.yaml | 2 + .../proposal.md | 39 ++++++++++ .../tests.md | 17 +++++ .../pluggableWidgets/image-web/CHANGELOG.md | 2 + .../pluggableWidgets/image-web/package.json | 2 +- .../pluggableWidgets/image-web/src/Image.xml | 4 +- .../image-web/src/__tests__/Image.spec.tsx | 76 +++++++++++++++++++ .../image-web/typings/ImageProps.d.ts | 6 +- 8 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 openspec/changes/fix-image-allow-upload-deprecation/.openspec.yaml create mode 100644 openspec/changes/fix-image-allow-upload-deprecation/proposal.md create mode 100644 openspec/changes/fix-image-allow-upload-deprecation/tests.md create mode 100644 packages/pluggableWidgets/image-web/src/__tests__/Image.spec.tsx diff --git a/openspec/changes/fix-image-allow-upload-deprecation/.openspec.yaml b/openspec/changes/fix-image-allow-upload-deprecation/.openspec.yaml new file mode 100644 index 0000000000..e7ed6cbc07 --- /dev/null +++ b/openspec/changes/fix-image-allow-upload-deprecation/.openspec.yaml @@ -0,0 +1,2 @@ +schema: tdd-refactor +created: 2026-07-23 diff --git a/openspec/changes/fix-image-allow-upload-deprecation/proposal.md b/openspec/changes/fix-image-allow-upload-deprecation/proposal.md new file mode 100644 index 0000000000..a6fe237fa9 --- /dev/null +++ b/openspec/changes/fix-image-allow-upload-deprecation/proposal.md @@ -0,0 +1,39 @@ +## Why + +The Image widget's `imageObject` and `defaultImageDynamic` properties are declared as `type="image"` without `allowUpload`. Mendix has deprecated this form of the image/file property type — `DynamicValue` will be removed in Mendix 12 — and apps using the widget on Studio Pro 11.8+ already see a deprecation warning at design time. The fix is to set `allowUpload="true"` on both properties, per Mendix's own migration guidance. + +## What Changes + +- `allowUpload="true"` added to `imageObject` and `defaultImageDynamic` in `Image.xml`. +- Generated typings (`ImageProps.d.ts`) change `imageObject`/`defaultImageDynamic` from `DynamicValue` to `EditableImageValue`. +- `Image.tsx` reads `.status` and `.value.uri` off both props today — `EditableImageValue` exposes the same shape (extends the same status/value base as `DynamicValue`), so no logic change is required. Widget stays display-only; no upload UI is added. +- `package.json` `marketplace.minimumMXVersion` bumped to `11.12.0` (LTS, confirmed with ticket reporter — 11.11 has native `allowUpload` support but 11.12 is the intended floor). +- CHANGELOG entry added under `[Unreleased]`. Widget version stays unbumped for now — version bumps happen at release time per repo convention; this change requires a major bump (2.0.0) once released, since `minimumMXVersion` is raised. + +Verified in Studio Pro (11.12, fresh test app) that `allowUpload="true"` does not remove the "Static" image-source option — both Static and Dynamic configuration remain available in the property editor, so existing apps configuring `imageObject`/`defaultImageDynamic` as a static asset are not broken by this change. + +## Capabilities + +### New Capabilities + + + +### Modified Capabilities + + + +## Root cause + +Widget XML predates the `allowUpload` attribute (introduced Mendix 11.8, native widget support 11.11). Not fixed proactively when the attribute was introduced; ticket WC-3471 flags the resulting deprecation warning. + +## Impact + +- **Files**: + - `packages/pluggableWidgets/image-web/src/Image.xml` + - `packages/pluggableWidgets/image-web/typings/ImageProps.d.ts` (regenerated) + - `packages/pluggableWidgets/image-web/package.json` (version, minimumMXVersion) + - `packages/pluggableWidgets/image-web/CHANGELOG.md` +- **Behavior**: none — widget remains display-only, no upload UI added, no runtime logic change +- **Studio Pro UX**: none — Static/Dynamic image-source configuration both remain available (manually verified) +- **Breaking change**: minimum Mendix version raised from 9.24.0 to 11.12.0 — apps on older Mendix versions cannot upgrade to this widget version once released +- **Affected widget**: `@mendix/image-web` — requires a major version bump (2.0.0) at release time; not bumped in this change diff --git a/openspec/changes/fix-image-allow-upload-deprecation/tests.md b/openspec/changes/fix-image-allow-upload-deprecation/tests.md new file mode 100644 index 0000000000..a94aa7749a --- /dev/null +++ b/openspec/changes/fix-image-allow-upload-deprecation/tests.md @@ -0,0 +1,17 @@ +- [x] **Container renders the bound image when imageObject is Available** + - **Type:** unit + - **Given:** `ImageContainerProps` with `datasource: "image"`, `imageObject` as an `EditableImageValue`-shaped object with `status: ValueStatus.Available` and `value.uri: "https://example.com/a.png"` + - **When:** `Image` (src/Image.tsx container) is rendered + - **Then:** the rendered `` `src` equals `"https://example.com/a.png"` + +- [x] **Container falls back to defaultImageDynamic when imageObject is Unavailable** + - **Type:** unit + - **Given:** `imageObject` with `status: ValueStatus.Unavailable`, `defaultImageDynamic` as `EditableImageValue`-shaped with `status: ValueStatus.Available` and `value.uri: "https://example.com/default.png"` + - **When:** `Image` is rendered + - **Then:** the rendered `` `src` equals `"https://example.com/default.png"` + +- [x] **Container renders nothing bound when both imageObject and defaultImageDynamic are unavailable** + - **Type:** unit + - **Given:** `imageObject` with `status: ValueStatus.Unavailable`, `defaultImageDynamic` undefined + - **When:** `Image` is rendered + - **Then:** the rendered image element has no meaningful `src` (placeholder/empty state), matching current behavior before this change diff --git a/packages/pluggableWidgets/image-web/CHANGELOG.md b/packages/pluggableWidgets/image-web/CHANGELOG.md index 6c60678c6d..012f7338b3 100644 --- a/packages/pluggableWidgets/image-web/CHANGELOG.md +++ b/packages/pluggableWidgets/image-web/CHANGELOG.md @@ -16,6 +16,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - 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. +- We fixed a deprecation warning by migrating the image source properties to the `allowUpload` property type. This raises the minimum supported Mendix version to 11.12.0. + ## [1.5.1] - 2025-10-29 ### Fixed diff --git a/packages/pluggableWidgets/image-web/package.json b/packages/pluggableWidgets/image-web/package.json index bf4bcc3cac..0c2adc483c 100644 --- a/packages/pluggableWidgets/image-web/package.json +++ b/packages/pluggableWidgets/image-web/package.json @@ -17,7 +17,7 @@ }, "packagePath": "com.mendix.widget.web", "marketplace": { - "minimumMXVersion": "9.24.0", + "minimumMXVersion": "11.12.0", "appNumber": 118579, "appName": "Image", "reactReady": true diff --git a/packages/pluggableWidgets/image-web/src/Image.xml b/packages/pluggableWidgets/image-web/src/Image.xml index f31738cc39..1692a5f0ae 100644 --- a/packages/pluggableWidgets/image-web/src/Image.xml +++ b/packages/pluggableWidgets/image-web/src/Image.xml @@ -17,11 +17,11 @@ Icon - + Image source - + Default image This is the image that is displayed if no image is uploaded. diff --git a/packages/pluggableWidgets/image-web/src/__tests__/Image.spec.tsx b/packages/pluggableWidgets/image-web/src/__tests__/Image.spec.tsx new file mode 100644 index 0000000000..32f6fdd73a --- /dev/null +++ b/packages/pluggableWidgets/image-web/src/__tests__/Image.spec.tsx @@ -0,0 +1,76 @@ +import { render } from "@testing-library/react"; +import { ValueStatus } from "mendix"; +import { ImageContainerProps } from "../../typings/ImageProps"; +import { Image } from "../Image"; + +type ImageProp = NonNullable; + +function makeImageValue(overrides: Partial = {}): ImageProp { + return { + status: ValueStatus.Available, + value: { uri: "https://example.com/a.png", name: "a.png" }, + readOnly: false, + validation: undefined, + setValidator: jest.fn(), + setValue: jest.fn(), + setThumbnailSize: jest.fn(), + ...overrides + } as ImageProp; +} + +function makeProps(overrides: Partial = {}): ImageContainerProps { + return { + name: "image1", + class: "", + style: undefined, + tabIndex: 0, + datasource: "image", + isBackgroundImage: false, + onClickType: "action", + widthUnit: "auto", + width: 100, + heightUnit: "auto", + height: 100, + minHeightUnit: "none", + minHeight: 0, + maxHeightUnit: "none", + maxHeight: 0, + iconSize: 14, + displayAs: "fullImage", + responsive: true, + ...overrides + } as ImageContainerProps; +} + +describe("Image container", () => { + it("renders the bound image when imageObject is Available", () => { + const { getByRole } = render(); + const image = getByRole("img") as HTMLImageElement; + expect(image.src).toBe("https://example.com/a.png"); + }); + + it("falls back to defaultImageDynamic when imageObject is Unavailable", () => { + const { getByRole } = render( + + ); + const image = getByRole("img") as HTMLImageElement; + expect(image.src).toBe("https://example.com/default.png"); + }); + + it("renders no image src when both imageObject and defaultImageDynamic are unavailable", () => { + const { container } = render( + + ); + const image = container.querySelector("img"); + expect(image?.getAttribute("src")).toBeFalsy(); + }); +}); diff --git a/packages/pluggableWidgets/image-web/typings/ImageProps.d.ts b/packages/pluggableWidgets/image-web/typings/ImageProps.d.ts index 0d3a7d156a..59472fd39e 100644 --- a/packages/pluggableWidgets/image-web/typings/ImageProps.d.ts +++ b/packages/pluggableWidgets/image-web/typings/ImageProps.d.ts @@ -3,7 +3,7 @@ * WARNING: All changes made to this file will be overwritten * @author Mendix Widgets Framework Team */ -import { ActionValue, DynamicValue, WebIcon, WebImage } from "mendix"; +import { ActionValue, DynamicValue, EditableImageValue, WebIcon, WebImage } from "mendix"; import { ComponentType, CSSProperties, ReactNode } from "react"; export type DatasourceEnum = "image" | "imageUrl" | "icon"; @@ -26,8 +26,8 @@ export interface ImageContainerProps { style?: CSSProperties; tabIndex?: number; datasource: DatasourceEnum; - imageObject?: DynamicValue; - defaultImageDynamic?: DynamicValue; + imageObject?: EditableImageValue; + defaultImageDynamic?: EditableImageValue; imageUrl?: DynamicValue; imageIcon?: DynamicValue; isBackgroundImage: boolean; From 2700e53412bb407669078cedd1f9e94da81e265a Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Mon, 27 Jul 2026 12:46:29 +0200 Subject: [PATCH 2/7] chore(deps): unify mendix package version across workspace --- package.json | 1 + packages/shared/filter-commons/package.json | 2 +- .../package.json | 2 +- .../widget-plugin-filtering/package.json | 2 +- .../widget-plugin-test-utils/package.json | 2 +- pnpm-lock.yaml | 25 +++++++------------ 6 files changed, 14 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 655c02503a..bb0548f3a1 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "d3-color@<3.1.0": ">=3.1.0", "fast-uri": "3.1.4", "glob@10": "^10.5.0", + "mendix": "11.10.0", "react": ">=18.0.0 <19.0.0", "react-big-calendar@1>clsx": "2.1.1", "react-dom": ">=18.0.0 <19.0.0", diff --git a/packages/shared/filter-commons/package.json b/packages/shared/filter-commons/package.json index 55f1d7a872..32d195cee7 100644 --- a/packages/shared/filter-commons/package.json +++ b/packages/shared/filter-commons/package.json @@ -32,7 +32,7 @@ "test": "jest" }, "dependencies": { - "mendix": "^10.24.75382" + "mendix": "*" }, "peerDependencies": { "mobx": "6.12.3", diff --git a/packages/shared/widget-plugin-dropdown-filter/package.json b/packages/shared/widget-plugin-dropdown-filter/package.json index 350d93fae0..513498cca2 100644 --- a/packages/shared/widget-plugin-dropdown-filter/package.json +++ b/packages/shared/widget-plugin-dropdown-filter/package.json @@ -38,7 +38,7 @@ "@mendix/widget-plugin-hooks": "workspace:*", "@mendix/widget-plugin-mobx-kit": "workspace:^", "downshift": "^9.0.9", - "mendix": "^10.24.75382", + "mendix": "*", "mobx": "6.12.3", "mobx-react-lite": "4.0.7" }, diff --git a/packages/shared/widget-plugin-filtering/package.json b/packages/shared/widget-plugin-filtering/package.json index 8a761b0340..f0983503b4 100644 --- a/packages/shared/widget-plugin-filtering/package.json +++ b/packages/shared/widget-plugin-filtering/package.json @@ -41,7 +41,7 @@ "@mendix/widget-plugin-mobx-kit": "workspace:^", "@mendix/widget-plugin-platform": "workspace:*", "downshift": "^9.0.8", - "mendix": "10.24.75382", + "mendix": "*", "mobx": "6.12.3", "mobx-react-lite": "4.0.7" }, diff --git a/packages/shared/widget-plugin-test-utils/package.json b/packages/shared/widget-plugin-test-utils/package.json index b1b034875b..c7d4ca5c3e 100644 --- a/packages/shared/widget-plugin-test-utils/package.json +++ b/packages/shared/widget-plugin-test-utils/package.json @@ -30,7 +30,7 @@ "test": "jest" }, "dependencies": { - "mendix": "10.24.75382" + "mendix": "*" }, "devDependencies": { "@mendix/eslint-config-web-widgets": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23566d6e47..b94bef99b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,7 @@ overrides: d3-color@<3.1.0: '>=3.1.0' fast-uri: 3.1.4 glob@10: ^10.5.0 + mendix: 11.10.0 react: '>=18.0.0 <19.0.0' react-big-calendar@1>clsx: 2.1.1 react-dom: '>=18.0.0 <19.0.0' @@ -2772,8 +2773,8 @@ importers: packages/shared/filter-commons: dependencies: mendix: - specifier: ^10.24.75382 - version: 10.24.75382 + specifier: 11.10.0 + version: 11.10.0 mobx: specifier: 6.12.3 version: 6.12.3(patch_hash=39c55279e8f75c9a322eba64dd22e1a398f621c64bbfc3632e55a97f46edfeb9) @@ -2874,8 +2875,8 @@ importers: specifier: ^9.0.9 version: 9.3.6(react@18.3.1) mendix: - specifier: ^10.24.75382 - version: 10.24.75382 + specifier: 11.10.0 + version: 11.10.0 mobx: specifier: 6.12.3 version: 6.12.3(patch_hash=39c55279e8f75c9a322eba64dd22e1a398f621c64bbfc3632e55a97f46edfeb9) @@ -2948,8 +2949,8 @@ importers: specifier: ^9.0.8 version: 9.3.6(react@18.3.1) mendix: - specifier: 10.24.75382 - version: 10.24.75382 + specifier: 11.10.0 + version: 11.10.0 mobx: specifier: 6.12.3 version: 6.12.3(patch_hash=39c55279e8f75c9a322eba64dd22e1a398f621c64bbfc3632e55a97f46edfeb9) @@ -3142,8 +3143,8 @@ importers: packages/shared/widget-plugin-test-utils: dependencies: mendix: - specifier: 10.24.75382 - version: 10.24.75382 + specifier: 11.10.0 + version: 11.10.0 devDependencies: '@mendix/eslint-config-web-widgets': specifier: workspace:* @@ -8221,9 +8222,6 @@ packages: memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} - mendix@10.24.75382: - resolution: {integrity: sha512-ICMxqkWUejsc3KeFD9BJYvC+T4soi/NB2iapwWPC7oN0lCrFx36upzwI4rU77oMdRHsrVSsFVYLBy7sJJOABHw==} - mendix@11.10.0: resolution: {integrity: sha512-OsLdgNJfhwG4/TcOIMMk0gu0ewHq3Xlv2CNW1YJy2ujqD19ngFvBVB9YTjYxmf+RMn1EG0FQYrn6zCSFzmbL4A==} @@ -16801,11 +16799,6 @@ snapshots: memoize-one@6.0.0: {} - mendix@10.24.75382: - dependencies: - '@types/big.js': 6.2.2 - '@types/react': 19.2.17 - mendix@11.10.0: dependencies: '@types/big.js': 6.2.2 From 9bb530d6a27fe891c904c41e48d314f4e50d97bb Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Mon, 27 Jul 2026 12:46:43 +0200 Subject: [PATCH 3/7] test(test-utils): add EditableImageValueBuilder --- .../src/builders/EditableImageValueBuilder.ts | 44 +++++++++++++++++++ .../widget-plugin-test-utils/src/main.ts | 2 + .../src/primitives/editableImage.ts | 24 ++++++++++ 3 files changed, 70 insertions(+) create mode 100644 packages/shared/widget-plugin-test-utils/src/builders/EditableImageValueBuilder.ts create mode 100644 packages/shared/widget-plugin-test-utils/src/primitives/editableImage.ts diff --git a/packages/shared/widget-plugin-test-utils/src/builders/EditableImageValueBuilder.ts b/packages/shared/widget-plugin-test-utils/src/builders/EditableImageValueBuilder.ts new file mode 100644 index 0000000000..8a4668b198 --- /dev/null +++ b/packages/shared/widget-plugin-test-utils/src/builders/EditableImageValueBuilder.ts @@ -0,0 +1,44 @@ +import type { EditableImageValue, ImageValue } from "mendix"; +import { Status } from "../constants.js"; +import { Writable } from "./type-utils.js"; + +export class EditableImageValueBuilder { + private readonly editableImageValue: Writable> = { + value: undefined, + status: Status.Available, + readOnly: false, + validation: undefined, + setValidator: jest.fn(), + setValue: jest.fn((value?: T) => this.withValue(value)), + setThumbnailSize: jest.fn() + }; + + withValue(value?: T): EditableImageValueBuilder { + this.editableImageValue.value = value; + return this; + } + + isReadOnly(): EditableImageValueBuilder { + this.editableImageValue.readOnly = true; + return this; + } + + isLoading(): EditableImageValueBuilder { + this.editableImageValue.status = Status.Loading; + return this.isReadOnly(); + } + + isUnavailable(): EditableImageValueBuilder { + this.editableImageValue.status = Status.Unavailable; + return this.isReadOnly(); + } + + withValidation(validation?: string): EditableImageValueBuilder { + this.editableImageValue.validation = validation; + return this; + } + + build(): EditableImageValue { + return this.editableImageValue; + } +} diff --git a/packages/shared/widget-plugin-test-utils/src/main.ts b/packages/shared/widget-plugin-test-utils/src/main.ts index d2b9b7e794..762c1f1990 100644 --- a/packages/shared/widget-plugin-test-utils/src/main.ts +++ b/packages/shared/widget-plugin-test-utils/src/main.ts @@ -1,3 +1,4 @@ +export * from "./builders/EditableImageValueBuilder.js"; export * from "./builders/EditableValueBuilder.js"; export * from "./builders/ReferenceValueBuilder.js"; export * from "./builders/ReferenceSetValueBuilder.js"; @@ -19,6 +20,7 @@ export * from "./primitives/action.js"; export * from "./primitives/attrId.js"; export * from "./primitives/dynamic.js"; export * from "./primitives/editable.js"; +export * from "./primitives/editableImage.js"; export * from "./primitives/obj.js"; export * from "./primitives/objArray.js"; export * from "./utils/IntersectionObserverStub.js"; diff --git a/packages/shared/widget-plugin-test-utils/src/primitives/editableImage.ts b/packages/shared/widget-plugin-test-utils/src/primitives/editableImage.ts new file mode 100644 index 0000000000..9f49e039f7 --- /dev/null +++ b/packages/shared/widget-plugin-test-utils/src/primitives/editableImage.ts @@ -0,0 +1,24 @@ +import type { EditableImageValue, ImageValue } from "mendix"; +import { EditableImageValueBuilder } from "../builders/EditableImageValueBuilder.js"; + +type Factory = (builder: EditableImageValueBuilder) => EditableImageValue; + +/** + * Short function to mock EditableImageValue. + * @param factory - optional factory. Accept builder as first argument. + * @returns {EditableImageValue} + */ +export function editableImage(factory?: Factory): EditableImageValue { + factory ??= builder => builder.build(); + return factory(new EditableImageValueBuilder()); +} + +/** + * Shorthand for builder.withValue(). + * @param value + */ +editableImage.with = _with; + +function _with(value: T): EditableImageValue { + return editableImage(builder => builder.withValue(value).build()); +} From df112ccf7471a6300f777f521689784922273ea5 Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Mon, 27 Jul 2026 12:46:53 +0200 Subject: [PATCH 4/7] test(image-web): use editableImage builder instead of hand-rolled mock --- .../image-web/src/__tests__/Image.spec.tsx | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/packages/pluggableWidgets/image-web/src/__tests__/Image.spec.tsx b/packages/pluggableWidgets/image-web/src/__tests__/Image.spec.tsx index 32f6fdd73a..40c592546b 100644 --- a/packages/pluggableWidgets/image-web/src/__tests__/Image.spec.tsx +++ b/packages/pluggableWidgets/image-web/src/__tests__/Image.spec.tsx @@ -1,23 +1,8 @@ import { render } from "@testing-library/react"; -import { ValueStatus } from "mendix"; +import { editableImage } from "@mendix/widget-plugin-test-utils"; import { ImageContainerProps } from "../../typings/ImageProps"; import { Image } from "../Image"; -type ImageProp = NonNullable; - -function makeImageValue(overrides: Partial = {}): ImageProp { - return { - status: ValueStatus.Available, - value: { uri: "https://example.com/a.png", name: "a.png" }, - readOnly: false, - validation: undefined, - setValidator: jest.fn(), - setValue: jest.fn(), - setThumbnailSize: jest.fn(), - ...overrides - } as ImageProp; -} - function makeProps(overrides: Partial = {}): ImageContainerProps { return { name: "image1", @@ -44,7 +29,9 @@ function makeProps(overrides: Partial = {}): ImageContainer describe("Image container", () => { it("renders the bound image when imageObject is Available", () => { - const { getByRole } = render(); + const { getByRole } = render( + + ); const image = getByRole("img") as HTMLImageElement; expect(image.src).toBe("https://example.com/a.png"); }); @@ -53,8 +40,8 @@ describe("Image container", () => { const { getByRole } = render( b.isUnavailable().build()), + defaultImageDynamic: editableImage.with({ uri: "https://example.com/default.png" }) })} /> ); @@ -66,7 +53,7 @@ describe("Image container", () => { const { container } = render( b.isUnavailable().build()) })} /> ); From c997ab55326c9db038baee81d4f1de717fbe3bcf Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Mon, 27 Jul 2026 12:47:01 +0200 Subject: [PATCH 5/7] chore(image-web): move openspec change --- .../src/__tests__/ImageCropper.spec.tsx | 10 ++-------- .../src/__tests__/ImageCropperGrayscale.spec.tsx | 13 ++----------- .../__tests__/ImageCropperMultiInstance.spec.tsx | 13 ++----------- .../src/__tests__/ImageCropperRotation.spec.tsx | 13 ++----------- .../src/stores/__tests__/ImageCropperStore.spec.ts | 10 ++-------- .../.openspec.yaml | 0 .../fix-image-allow-upload-deprecation/proposal.md | 0 .../fix-image-allow-upload-deprecation/tests.md | 0 8 files changed, 10 insertions(+), 49 deletions(-) rename {openspec => packages/pluggableWidgets/image-web/openspec}/changes/fix-image-allow-upload-deprecation/.openspec.yaml (100%) rename {openspec => packages/pluggableWidgets/image-web/openspec}/changes/fix-image-allow-upload-deprecation/proposal.md (100%) rename {openspec => packages/pluggableWidgets/image-web/openspec}/changes/fix-image-allow-upload-deprecation/tests.md (100%) diff --git a/packages/pluggableWidgets/image-cropper-web/src/__tests__/ImageCropper.spec.tsx b/packages/pluggableWidgets/image-cropper-web/src/__tests__/ImageCropper.spec.tsx index 6bf764e3a8..93c2d32006 100644 --- a/packages/pluggableWidgets/image-cropper-web/src/__tests__/ImageCropper.spec.tsx +++ b/packages/pluggableWidgets/image-cropper-web/src/__tests__/ImageCropper.spec.tsx @@ -3,7 +3,7 @@ import { Big } from "big.js"; import { ValueStatus } from "mendix"; import { Ref } from "react"; import type { Crop, PixelCrop } from "react-image-crop"; -import { actionValue, dynamic } from "@mendix/widget-plugin-test-utils"; +import { actionValue, dynamic, editableImage } from "@mendix/widget-plugin-test-utils"; import type { ImageCropperContainerProps } from "../../typings/ImageCropperProps"; // Capture the container's callbacks via a mocked CropArea. Real ReactCrop only fires @@ -67,13 +67,7 @@ const PERCENT_CROP: Crop = { unit: "%", x: 5, y: 5, width: 50, height: 50 }; function makeImageProp(overrides: Partial = {}): ImageProp { return { - status: ValueStatus.Available, - value: { uri: "http://localhost/img.png", name: "img.png" } as WebImage, - readOnly: false, - validation: undefined, - setValidator: jest.fn(), - setValue: jest.fn(), - setThumbnailSize: jest.fn(), + ...editableImage.with({ uri: "http://localhost/img.png", name: "img.png" }), ...overrides } as ImageProp; } diff --git a/packages/pluggableWidgets/image-cropper-web/src/__tests__/ImageCropperGrayscale.spec.tsx b/packages/pluggableWidgets/image-cropper-web/src/__tests__/ImageCropperGrayscale.spec.tsx index e43fe42904..76ff907009 100644 --- a/packages/pluggableWidgets/image-cropper-web/src/__tests__/ImageCropperGrayscale.spec.tsx +++ b/packages/pluggableWidgets/image-cropper-web/src/__tests__/ImageCropperGrayscale.spec.tsx @@ -1,9 +1,8 @@ import { act, fireEvent, render, screen } from "@testing-library/react"; import { Big } from "big.js"; -import { ValueStatus } from "mendix"; import { Ref } from "react"; import type { Crop, PixelCrop } from "react-image-crop"; -import { actionValue } from "@mendix/widget-plugin-test-utils"; +import { actionValue, editableImage } from "@mendix/widget-plugin-test-utils"; import type { ImageCropperContainerProps } from "../../typings/ImageCropperProps"; // Integration test: proves grayscale reversibility after rotate. @@ -76,15 +75,7 @@ const PIXEL_CROP: PixelCrop = { unit: "px", x: 10, y: 10, width: 100, height: 10 const PERCENT_CROP: Crop = { unit: "%", x: 5, y: 5, width: 50, height: 50 }; function makeImageProp(): ImageProp { - return { - status: ValueStatus.Available, - value: { uri: "http://localhost/img.png", name: "img.png" } as WebImage, - readOnly: false, - validation: undefined, - setValidator: jest.fn(), - setValue: jest.fn(), - setThumbnailSize: jest.fn() - } as ImageProp; + return editableImage.with({ uri: "http://localhost/img.png", name: "img.png" }) as ImageProp; } function makeProps(overrides: Partial = {}): ImageCropperContainerProps { diff --git a/packages/pluggableWidgets/image-cropper-web/src/__tests__/ImageCropperMultiInstance.spec.tsx b/packages/pluggableWidgets/image-cropper-web/src/__tests__/ImageCropperMultiInstance.spec.tsx index 49af30088f..c3e1baed60 100644 --- a/packages/pluggableWidgets/image-cropper-web/src/__tests__/ImageCropperMultiInstance.spec.tsx +++ b/packages/pluggableWidgets/image-cropper-web/src/__tests__/ImageCropperMultiInstance.spec.tsx @@ -1,9 +1,8 @@ import { act, render } from "@testing-library/react"; import { Big } from "big.js"; -import { ValueStatus } from "mendix"; import { Ref } from "react"; import type { Crop, PixelCrop } from "react-image-crop"; -import { actionValue } from "@mendix/widget-plugin-test-utils"; +import { actionValue, editableImage } from "@mendix/widget-plugin-test-utils"; import type { ImageCropperContainerProps } from "../../typings/ImageCropperProps"; // Multi-instance integration test: verifies that auto-commit only fires for the @@ -68,15 +67,7 @@ const PIXEL_CROP: PixelCrop = { unit: "px", x: 10, y: 10, width: 100, height: 10 const PERCENT_CROP: Crop = { unit: "%", x: 5, y: 5, width: 50, height: 50 }; function makeImageProp(): ImageProp { - return { - status: ValueStatus.Available, - value: { uri: "http://localhost/img.png", name: "img.png" } as WebImage, - readOnly: false, - validation: undefined, - setValidator: jest.fn(), - setValue: jest.fn(), - setThumbnailSize: jest.fn() - } as ImageProp; + return editableImage.with({ uri: "http://localhost/img.png", name: "img.png" }) as ImageProp; } function makeProps(overrides: Partial = {}): ImageCropperContainerProps { diff --git a/packages/pluggableWidgets/image-cropper-web/src/__tests__/ImageCropperRotation.spec.tsx b/packages/pluggableWidgets/image-cropper-web/src/__tests__/ImageCropperRotation.spec.tsx index b3806b507e..78e205d995 100644 --- a/packages/pluggableWidgets/image-cropper-web/src/__tests__/ImageCropperRotation.spec.tsx +++ b/packages/pluggableWidgets/image-cropper-web/src/__tests__/ImageCropperRotation.spec.tsx @@ -1,9 +1,8 @@ import { act, fireEvent, render, screen } from "@testing-library/react"; import { Big } from "big.js"; -import { ValueStatus } from "mendix"; import { Ref } from "react"; import type { Crop, PixelCrop } from "react-image-crop"; -import { actionValue } from "@mendix/widget-plugin-test-utils"; +import { actionValue, editableImage } from "@mendix/widget-plugin-test-utils"; import type { ImageCropperContainerProps } from "../../typings/ImageCropperProps"; // Integration test: proves the rotate/grayscale actions reach the right util with the right args. @@ -85,15 +84,7 @@ const PIXEL_CROP: PixelCrop = { unit: "px", x: 10, y: 10, width: 100, height: 10 const PERCENT_CROP: Crop = { unit: "%", x: 5, y: 5, width: 50, height: 50 }; function makeImageProp(): ImageProp { - return { - status: ValueStatus.Available, - value: { uri: "http://localhost/img.png", name: "img.png" } as WebImage, - readOnly: false, - validation: undefined, - setValidator: jest.fn(), - setValue: jest.fn(), - setThumbnailSize: jest.fn() - } as ImageProp; + return editableImage.with({ uri: "http://localhost/img.png", name: "img.png" }) as ImageProp; } function makeProps(overrides: Partial = {}): ImageCropperContainerProps { diff --git a/packages/pluggableWidgets/image-cropper-web/src/stores/__tests__/ImageCropperStore.spec.ts b/packages/pluggableWidgets/image-cropper-web/src/stores/__tests__/ImageCropperStore.spec.ts index f8df72000c..60a6566678 100644 --- a/packages/pluggableWidgets/image-cropper-web/src/stores/__tests__/ImageCropperStore.spec.ts +++ b/packages/pluggableWidgets/image-cropper-web/src/stores/__tests__/ImageCropperStore.spec.ts @@ -1,8 +1,8 @@ import { Big } from "big.js"; -import { ValueStatus } from "mendix"; import { action, makeObservable, observable } from "mobx"; import type { Crop, PixelCrop } from "react-image-crop"; import { DerivedPropsGate } from "@mendix/widget-plugin-mobx-kit/main"; +import { editableImage } from "@mendix/widget-plugin-test-utils"; import type { ImageCropperContainerProps } from "../../../typings/ImageCropperProps"; // The store calls cropImage/rotateImage (async canvas work). Mock them so the spec asserts @@ -33,13 +33,7 @@ type WebImage = NonNullable; function makeImageProp(overrides: Partial = {}): ImageProp { return { - status: ValueStatus.Available, - value: { uri: "http://localhost/img.png", name: "img.png" } as WebImage, - readOnly: false, - validation: undefined, - setValidator: jest.fn(), - setValue: jest.fn(), - setThumbnailSize: jest.fn(), + ...editableImage.with({ uri: "http://localhost/img.png", name: "img.png" }), ...overrides } as unknown as ImageProp; } diff --git a/openspec/changes/fix-image-allow-upload-deprecation/.openspec.yaml b/packages/pluggableWidgets/image-web/openspec/changes/fix-image-allow-upload-deprecation/.openspec.yaml similarity index 100% rename from openspec/changes/fix-image-allow-upload-deprecation/.openspec.yaml rename to packages/pluggableWidgets/image-web/openspec/changes/fix-image-allow-upload-deprecation/.openspec.yaml diff --git a/openspec/changes/fix-image-allow-upload-deprecation/proposal.md b/packages/pluggableWidgets/image-web/openspec/changes/fix-image-allow-upload-deprecation/proposal.md similarity index 100% rename from openspec/changes/fix-image-allow-upload-deprecation/proposal.md rename to packages/pluggableWidgets/image-web/openspec/changes/fix-image-allow-upload-deprecation/proposal.md diff --git a/openspec/changes/fix-image-allow-upload-deprecation/tests.md b/packages/pluggableWidgets/image-web/openspec/changes/fix-image-allow-upload-deprecation/tests.md similarity index 100% rename from openspec/changes/fix-image-allow-upload-deprecation/tests.md rename to packages/pluggableWidgets/image-web/openspec/changes/fix-image-allow-upload-deprecation/tests.md From 6f2b1e7c5b1f2f4a1865e84181cf83237dd69ea2 Mon Sep 17 00:00:00 2001 From: leonardomendix Date: Tue, 28 Jul 2026 14:09:16 +0200 Subject: [PATCH 6/7] test(image-web): update e2e script to run Mx 11.12.0 --- automation/run-e2e/docker/mxbuild.Dockerfile | 4 +++- packages/pluggableWidgets/image-web/package.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/automation/run-e2e/docker/mxbuild.Dockerfile b/automation/run-e2e/docker/mxbuild.Dockerfile index c6a3934271..739fdc3985 100644 --- a/automation/run-e2e/docker/mxbuild.Dockerfile +++ b/automation/run-e2e/docker/mxbuild.Dockerfile @@ -3,6 +3,8 @@ FROM --platform=$BUILDPLATFORM eclipse-temurin:21-jdk-jammy ARG MENDIX_VERSION ARG BUILDPLATFORM +ENV LD_PRELOAD=libfontconfig.so.1 + SHELL ["/bin/bash", "-c"] RUN \ echo "Downloading mxbuild ${MENDIX_VERSION} and docker building for ${BUILDPLATFORM}..." \ @@ -26,7 +28,7 @@ echo "Downloading mxbuild ${MENDIX_VERSION} and docker building for ${BUILDPLATF \ rm -rf /var/lib/apt/lists/* && \ apt-get update --allow-insecure-repositories -qqy && \ - apt-get install -qqy --allow-unauthenticated libicu70 && \ + apt-get install -qqy --allow-unauthenticated libicu70 libgdiplus libfontconfig1 && \ apt-get -qqy remove --auto-remove wget && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ diff --git a/packages/pluggableWidgets/image-web/package.json b/packages/pluggableWidgets/image-web/package.json index 0c2adc483c..f094744e2d 100644 --- a/packages/pluggableWidgets/image-web/package.json +++ b/packages/pluggableWidgets/image-web/package.json @@ -31,7 +31,7 @@ "create-gh-release": "rui-create-gh-release", "create-translation": "rui-create-translation", "dev": "pluggable-widgets-tools start:web", - "e2e": "run-e2e ci", + "e2e": "MENDIX_VERSION=11.12.0 run-e2e ci", "e2edev": "run-e2e dev --with-preps", "format": "prettier --ignore-path ./node_modules/@mendix/prettier-config-web-widgets/global-prettierignore --write .", "lint": "eslint src/ package.json", From 8ab1f8636dad1cdf755fd1c2f88ad8fa8007472b Mon Sep 17 00:00:00 2001 From: Yordan Stoyanov Date: Wed, 29 Jul 2026 10:34:38 +0200 Subject: [PATCH 7/7] test(image-web): use editableImage builder in getImageProps.spec ImageContainerProps now types imageObject/defaultImageDynamic as EditableImageValue, but the spec still built inputs with dynamic.available/loading/unavailable (DynamicValue), causing tsc failures (TS2322) and a failing test suite in CI. Migrate the datasource: "image" cases to the editableImage builder, matching the pattern already used in image-cropper tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/utils/__tests__/getImageProps.spec.ts | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/pluggableWidgets/image-web/src/utils/__tests__/getImageProps.spec.ts b/packages/pluggableWidgets/image-web/src/utils/__tests__/getImageProps.spec.ts index fd2c2a0c8d..93bdef4d47 100644 --- a/packages/pluggableWidgets/image-web/src/utils/__tests__/getImageProps.spec.ts +++ b/packages/pluggableWidgets/image-web/src/utils/__tests__/getImageProps.spec.ts @@ -1,5 +1,5 @@ import { WebIcon, WebImage } from "mendix"; -import { dynamic } from "@mendix/widget-plugin-test-utils"; +import { dynamic, editableImage } from "@mendix/widget-plugin-test-utils"; import { getImageProps, GetImagePropsInput } from "../getImageProps"; const webImage = (uri: string): WebImage => ({ uri, name: "test.jpg" }); @@ -9,7 +9,7 @@ describe("getImageProps", () => { it("returns the main image URI when main image is available", () => { const input: GetImagePropsInput = { datasource: "image", - imageObject: dynamic.available(webImage("https://example.com/main.jpg")) + imageObject: editableImage.with(webImage("https://example.com/main.jpg")) }; expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/main.jpg" }); }); @@ -17,7 +17,9 @@ describe("getImageProps", () => { it("returns the main image URI when main image is loading (uri present)", () => { const input: GetImagePropsInput = { datasource: "image", - imageObject: dynamic.loading(webImage("https://example.com/main.jpg")) + imageObject: editableImage(b => + b.isLoading().withValue(webImage("https://example.com/main.jpg")).build() + ) }; expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/main.jpg" }); }); @@ -25,7 +27,7 @@ describe("getImageProps", () => { it("returns undefined image when main image is loading (no uri yet)", () => { const input: GetImagePropsInput = { datasource: "image", - imageObject: dynamic.loading() + imageObject: editableImage(b => b.isLoading().build()) }; expect(getImageProps(input)).toEqual({ type: "image", image: undefined }); }); @@ -33,8 +35,8 @@ describe("getImageProps", () => { it("falls back to defaultImage when main image is unavailable and fallback is available", () => { const input: GetImagePropsInput = { datasource: "image", - imageObject: dynamic.unavailable(), - defaultImageDynamic: dynamic.available(webImage("https://example.com/fallback.jpg")) + imageObject: editableImage(b => b.isUnavailable().build()), + defaultImageDynamic: editableImage.with(webImage("https://example.com/fallback.jpg")) }; expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/fallback.jpg" }); }); @@ -42,8 +44,10 @@ describe("getImageProps", () => { it("falls back to defaultImage when main image is unavailable and fallback is loading", () => { const input: GetImagePropsInput = { datasource: "image", - imageObject: dynamic.unavailable(), - defaultImageDynamic: dynamic.loading(webImage("https://example.com/fallback.jpg")) + imageObject: editableImage(b => b.isUnavailable().build()), + defaultImageDynamic: editableImage(b => + b.isLoading().withValue(webImage("https://example.com/fallback.jpg")).build() + ) }; expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/fallback.jpg" }); }); @@ -51,8 +55,8 @@ describe("getImageProps", () => { it("returns undefined image when both main image and fallback are unavailable", () => { const input: GetImagePropsInput = { datasource: "image", - imageObject: dynamic.unavailable(), - defaultImageDynamic: dynamic.unavailable() + imageObject: editableImage(b => b.isUnavailable().build()), + defaultImageDynamic: editableImage(b => b.isUnavailable().build()) }; expect(getImageProps(input)).toEqual({ type: "image", image: undefined }); }); @@ -65,8 +69,8 @@ describe("getImageProps", () => { it("does not use defaultImage when main image is available", () => { const input: GetImagePropsInput = { datasource: "image", - imageObject: dynamic.available(webImage("https://example.com/main.jpg")), - defaultImageDynamic: dynamic.available(webImage("https://example.com/fallback.jpg")) + imageObject: editableImage.with(webImage("https://example.com/main.jpg")), + defaultImageDynamic: editableImage.with(webImage("https://example.com/fallback.jpg")) }; expect(getImageProps(input)).toEqual({ type: "image", image: "https://example.com/main.jpg" }); });