Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4a5b6c4
feat(image-crop-web): scaffold package metadata
rahmanunver May 21, 2026
fccfd7f
fix(image-crop-web): align LICENSE copyright year with package.json
rahmanunver May 21, 2026
b0e19ac
chore(image-crop-web): add TypeScript and Rollup config
rahmanunver May 21, 2026
bce5543
feat(image-crop-web): declare widget XML properties
rahmanunver May 21, 2026
4bef6b0
feat(image-crop-web): generate typings and add minimal widget stubs
rahmanunver May 22, 2026
3eabd2f
chore(image-crop-web): remove legacy .eslintrc.js, flat config wins
rahmanunver May 22, 2026
7f62312
feat(image-crop-web): add aspect ratio resolver
rahmanunver May 22, 2026
b00a414
feat(image-crop-web): add cropImage canvas extraction utility
rahmanunver May 22, 2026
d185f32
chore(image-crop-web): align test script with sibling --projects conv…
rahmanunver May 22, 2026
015e7b7
feat(image-crop-web): add useWheelZoom hook with mode gating
rahmanunver May 22, 2026
13da656
feat(image-crop-web): add useImageCropState hook
rahmanunver May 22, 2026
98f3fcf
feat(image-crop-web): add CropButton, ZoomSlider, PreviewPane leaves
rahmanunver May 26, 2026
ba94c90
feat(image-crop-web): add CropArea with ReactCrop + zoom wrapper
rahmanunver May 26, 2026
af22b68
feat(image-crop-web): assemble container with state, area, preview, b…
rahmanunver May 26, 2026
f28c44a
feat(image-crop-web): bundle ReactCrop CSS via SCSS import
rahmanunver May 26, 2026
48eb336
test(image-crop-web): add container RTL tests for state guards
rahmanunver May 26, 2026
31f6be9
fix(image-crop-web): make primitive props required per Mendix XSD
rahmanunver May 26, 2026
0dd2436
docs(image-crop-web): add property descriptions for Studio Pro tooltips
rahmanunver May 26, 2026
bcd4ea9
feat(image-crop-web): add Studio Pro structure preview and pre-PR polish
rahmanunver May 27, 2026
7266146
feat(image-crop-web): scaffold package metadata
rahmanunver May 21, 2026
047d2c1
fix(image-crop-web): align LICENSE copyright year with package.json
rahmanunver May 21, 2026
df0179f
chore(image-crop-web): add TypeScript and Rollup config
rahmanunver May 21, 2026
1fd939a
feat(image-crop-web): declare widget XML properties
rahmanunver May 21, 2026
21bd669
feat(image-crop-web): generate typings and add minimal widget stubs
rahmanunver May 22, 2026
8a37921
chore(image-crop-web): remove legacy .eslintrc.js, flat config wins
rahmanunver May 22, 2026
470f95e
feat(image-crop-web): add aspect ratio resolver
rahmanunver May 22, 2026
d2980b2
feat(image-crop-web): add cropImage canvas extraction utility
rahmanunver May 22, 2026
502a233
chore(image-crop-web): align test script with sibling --projects conv…
rahmanunver May 22, 2026
0b8c2a8
feat(image-crop-web): add useWheelZoom hook with mode gating
rahmanunver May 22, 2026
74f69e9
feat(image-crop-web): add useImageCropState hook
rahmanunver May 22, 2026
1e71de9
feat(image-crop-web): add CropButton, ZoomSlider, PreviewPane leaves
rahmanunver May 26, 2026
21326b0
feat(image-crop-web): add CropArea with ReactCrop + zoom wrapper
rahmanunver May 26, 2026
9a74a5f
feat(image-crop-web): assemble container with state, area, preview, b…
rahmanunver May 26, 2026
dee55bc
feat(image-crop-web): bundle ReactCrop CSS via SCSS import
rahmanunver May 26, 2026
a77ce2e
test(image-crop-web): add container RTL tests for state guards
rahmanunver May 26, 2026
5f13460
fix(image-crop-web): make primitive props required per Mendix XSD
rahmanunver May 26, 2026
822ecad
docs(image-crop-web): add property descriptions for Studio Pro tooltips
rahmanunver May 26, 2026
b19bb26
feat(image-crop-web): add Studio Pro structure preview and pre-PR polish
rahmanunver May 27, 2026
97c25d2
feat(image-crop-web): add github labeler
rahmanunver May 27, 2026
cf660b8
Merge branch 'feat/image-crop-web' of github.com:mendix/web-widgets i…
rahmanunver May 27, 2026
dbe4d4c
fix(image-crop-web): pnpm-lock fix
rahmanunver May 27, 2026
3c3708a
fix(image-crop-web): correct readme language
rahmanunver May 27, 2026
f376009
fix(image-crop-web): adress comments
rahmanunver May 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/configs/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ combobox-web:
- packages/*/combobox-web/**/*
google-tag-web:
- packages/*/google-tag-web/**/*
image-crop-web:
- packages/*/image-crop-web/**/*

# Internals
shared:
Expand Down
3 changes: 3 additions & 0 deletions packages/pluggableWidgets/image-crop-web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist/
*.mpk
typings/
13 changes: 13 additions & 0 deletions packages/pluggableWidgets/image-crop-web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Changelog

All notable changes to this widget will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.0] - 2026-05-21

### Added

- Initial release. Crops a bound `EditableImageValue<WebImage>` attribute with rectangular or circular viewport, optional zoom (slider + wheel), live preview pane, and PNG/JPEG output. Replaces the legacy ImageCrop widget.
15 changes: 15 additions & 0 deletions packages/pluggableWidgets/image-crop-web/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
The Apache License v2.0

Copyright © Mendix Technology BV 2026. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
5 changes: 5 additions & 0 deletions packages/pluggableWidgets/image-crop-web/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Image Crop

Crops images bound to a Mendix image attribute. The cropped result is written back to the same attribute.

See the [Mendix Marketplace listing](https://marketplace.mendix.com/) for usage docs.
3 changes: 3 additions & 0 deletions packages/pluggableWidgets/image-crop-web/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import config from "@mendix/eslint-config-web-widgets/widget-ts.mjs";

export default config;
6 changes: 6 additions & 0 deletions packages/pluggableWidgets/image-crop-web/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const base = require("@mendix/pluggable-widgets-tools/test-config/jest.config.js");

module.exports = {
...base,
setupFilesAfterEnv: [...(base.setupFilesAfterEnv ?? []), require("path").join(__dirname, "jest.setup.ts")]
};
61 changes: 61 additions & 0 deletions packages/pluggableWidgets/image-crop-web/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Jest setup for image-crop-web tests.
*
* Problem: when `canvas` npm package is installed, jsdom uses node-canvas. Its `drawImage`
* rejects jsdom HTMLImageElement objects. Also, the test's `captureDrawImageCalls` helper spies on
* `CanvasRenderingContext2D.prototype.drawImage` — which must be the mock class prototype for the
* spy to fire.
*
* Fix:
* 1. Replace `global.CanvasRenderingContext2D` with the jest-canvas-mock class.
* 2. Override `HTMLCanvasElement.prototype.getContext` to return a MockCRC2D instance.
* This makes the context returned by our code an instance of MockCRC2D, so the spec's spy
* on `CanvasRenderingContext2D.prototype.drawImage` (which equals MockCRC2D.prototype.drawImage)
* fires correctly.
* 3. Override `HTMLCanvasElement.prototype.toBlob` to return a valid Blob synchronously
* (avoiding node-canvas toBuffer issues in tests).
*/

// eslint-disable-next-line @typescript-eslint/no-require-imports
const MockCRC2D = require("jest-canvas-mock/lib/classes/CanvasRenderingContext2D").default;
// eslint-disable-next-line @typescript-eslint/no-require-imports
const MockImageBitmap = require("jest-canvas-mock/lib/classes/ImageBitmap").default;

// Make global.CanvasRenderingContext2D the mock class so spec spies on the right prototype
(global as any).CanvasRenderingContext2D = MockCRC2D;
// MockCRC2D's drawImage references ImageBitmap globally — provide a stub if jsdom doesn't have it
if (!(global as any).ImageBitmap) {
(global as any).ImageBitmap = MockImageBitmap;
}

// Per-canvas context map for idempotency
const contextMap = new WeakMap<HTMLCanvasElement, InstanceType<typeof MockCRC2D>>();

// Patch HTMLCanvasElement.prototype.getContext — jsdom exposes this as a regular JS method
const origGetContext = HTMLCanvasElement.prototype.getContext;
(HTMLCanvasElement.prototype as any).getContext = function (
this: HTMLCanvasElement,
type: string,
...rest: unknown[]
): unknown {
if (type === "2d") {
if (!contextMap.has(this)) {
contextMap.set(this, new MockCRC2D(this));
}
return contextMap.get(this);
}
return (origGetContext as Function).apply(this, [type, ...rest]);
};

// Patch HTMLCanvasElement.prototype.toBlob to avoid node-canvas's toBuffer path
(HTMLCanvasElement.prototype as any).toBlob = function (
this: HTMLCanvasElement,
callback: (blob: Blob | null) => void,
type?: string
): void {
const mime = type === "image/jpeg" || type === "image/webp" ? type : "image/png";
const length = this.width * this.height * 4;
const data = new Uint8Array(length);
const blob = new Blob([data], { type: mime });
setTimeout(() => callback(blob), 0);
};
57 changes: 57 additions & 0 deletions packages/pluggableWidgets/image-crop-web/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "@mendix/image-crop-web",
"widgetName": "ImageCrop",
"version": "1.0.0",
"description": "Crop images bound to a Mendix image attribute",
"copyright": "© Mendix Technology BV 2026. All rights reserved.",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/mendix/web-widgets.git"
},
"config": {},
"mxpackage": {
"name": "ImageCrop",
"type": "widget",
"mpkName": "com.mendix.widget.web.ImageCrop.mpk"
},
"packagePath": "com.mendix.widget.web",
"marketplace": {
"minimumMXVersion": "10.21.0",
"appName": "Image Crop",
"appNumber": 1,
"reactReady": true
},
"testProject": {
"githubUrl": "https://github.com/mendix/testProjects",
"branchName": "image-crop-web"
},
"scripts": {
"build": "pluggable-widgets-tools build:web",
"create-gh-release": "rui-create-gh-release",
"create-translation": "rui-create-translation",
"dev": "pluggable-widgets-tools start:web",
"format": "prettier --ignore-path ./node_modules/@mendix/prettier-config-web-widgets/global-prettierignore --write .",
"lint": "eslint src/ package.json",
"publish-marketplace": "rui-publish-marketplace",
"release": "pluggable-widgets-tools release:web",
"start": "pluggable-widgets-tools start:server",
"test": "jest --projects jest.config.js",
"update-changelog": "rui-update-changelog-widget",
"verify": "rui-verify-package-format"
},
"dependencies": {
"classnames": "^2.5.1",
"react-image-crop": "^11.0.10"
},
"devDependencies": {
"@mendix/automation-utils": "workspace:*",
"@mendix/eslint-config-web-widgets": "workspace:*",
"@mendix/pluggable-widgets-tools": "*",
"@mendix/prettier-config-web-widgets": "workspace:*",
"@mendix/rollup-web-widgets": "workspace:*",
"@mendix/widget-plugin-platform": "workspace:*",
"@mendix/widget-plugin-test-utils": "workspace:*",
"jest-canvas-mock": "^2.5.2"
}
}
5 changes: 5 additions & 0 deletions packages/pluggableWidgets/image-crop-web/rollup.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import copyFiles from "@mendix/rollup-web-widgets/copyFiles.mjs";

export default args => {
return copyFiles(args);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { hidePropertiesIn, Properties } from "@mendix/pluggable-widgets-tools";
import {
StructurePreviewProps,
structurePreviewPalette
} from "@mendix/widget-plugin-platform/preview/structure-preview-api";
import { ImageCropPreviewProps } from "../typings/ImageCropProps";
import CropIconSvg from "./assets/crop-icon.svg";

export function getProperties(values: ImageCropPreviewProps, defaultProperties: Properties): Properties {
const propsToHide: Array<keyof ImageCropPreviewProps> = [];

if (values.aspectRatio !== "custom") {
propsToHide.push("customAspectWidth", "customAspectHeight");
}

if (!values.zoomEnabled) {
propsToHide.push("wheelZoomMode", "minZoom", "maxZoom");
}

if (!values.showPreview) {
propsToHide.push("previewWidth", "previewHeight");
}

if (values.outputFormat !== "jpeg") {
propsToHide.push("outputQuality");
}

hidePropertiesIn(defaultProperties, values, propsToHide);
return defaultProperties;
}

export function getPreview(values: ImageCropPreviewProps, isDarkMode: boolean): StructurePreviewProps {
const palette = structurePreviewPalette[isDarkMode ? "dark" : "light"];
const iconDocument = decodeURIComponent(CropIconSvg.replace("data:image/svg+xml,", ""));

return {
type: "Container",
borders: true,
borderRadius: 4,
backgroundColor: palette.background.containerFill,
children: [
{
type: "RowLayout",
columnSize: "grow",
padding: 12,
children: [
{
type: "Container",
grow: 0,
padding: 4,
children: [
{
type: "Image",
document: iconDocument,
width: 28,
height: 22
}
]
},
{
type: "Container",
grow: 1,
children: [
{
type: "Text",
content: "Image Crop",
bold: true,
fontColor: palette.text.primary,
fontSize: 10
},
{
type: "Text",
content: describeConfig(values),
fontColor: palette.text.secondary,
fontSize: 8
}
]
}
]
}
]
};
}

export function getCustomCaption(values: ImageCropPreviewProps): string {
const shape = values.cropShape === "circle" ? "Circle" : "Rectangle";
return `Image Crop (${shape})`;
}

function describeConfig(values: ImageCropPreviewProps): string {
const parts: string[] = [];
parts.push(values.cropShape === "circle" ? "Circle" : "Rectangle");
parts.push(aspectLabel(values));
parts.push(`${values.outputFormat.toUpperCase()} · ${values.outputSize === "viewport" ? "Viewport" : "Original"}`);
return parts.join(" · ");
}

function aspectLabel(values: ImageCropPreviewProps): string {
switch (values.aspectRatio) {
case "free":
return "Free aspect";
case "square":
return "1:1";
case "landscape16x9":
return "16:9";
case "landscape4x3":
return "4:3";
case "portrait3x4":
return "3:4";
case "custom":
return `${values.customAspectWidth}:${values.customAspectHeight}`;
default: {
const _exhaustive: never = values.aspectRatio;
return _exhaustive;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import classNames from "classnames";
import { ReactElement } from "react";
import { ImageCropPreviewProps } from "../typings/ImageCropProps";

export function preview(props: ImageCropPreviewProps): ReactElement {
return (
<div className={classNames(props.class, "widget-image-crop", "widget-image-crop--preview")}>
<div className="widget-image-crop__dropzone">
<div className="widget-image-crop__icon" />
<p className="widget-image-crop__label">Image Crop</p>
</div>
</div>
);
}

export function getPreviewCss(): string {
return require("./ui/ImageCrop.scss");
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions packages/pluggableWidgets/image-crop-web/src/ImageCrop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReactElement } from "react";
import { ImageCropContainerProps } from "../typings/ImageCropProps";
import { ImageCropContainer } from "./components/ImageCropContainer";
import "./ui/ImageCrop.scss";

export function ImageCrop(props: ImageCropContainerProps): ReactElement | null {
return <ImageCropContainer {...props} />;
}
Loading
Loading