Skip to content

Commit ffcbace

Browse files
committed
refactor(image-cropper-web): resolve zoom updater in store
Move the SetStateAction resolution out of the container and into ImageCropperStore.setZoom, which now accepts a value or updater and resolves the functional form against its own zoom. The container drops to setZoom={next => store.setZoom(next)}, making the store the single owner of current zoom. The functional-updater contract stays on the CropArea/ZoomContainer props (the wheel-zoom hook drives setZoom(prev => …) and the editor preview passes a no-op). Behavior unchanged. Also sync the image-cropper openspec: the "No image available" scenario now reflects the editable noImageCaption empty-state text.
1 parent cac0dc1 commit ffcbace

3 files changed

Lines changed: 9 additions & 4 deletions

File tree

packages/pluggableWidgets/image-cropper-web/openspec/specs/image-cropper/spec.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ same attribute via `setValue`.
2525
#### Scenario: No image available
2626

2727
- **WHEN** the bound image status is not `Available` or has no value
28-
- **THEN** the widget SHALL render an empty state reading "No image"
28+
- **THEN** the widget SHALL render an empty state showing the configurable `noImageCaption` (default "No uploaded image to crop")
2929

3030
#### Scenario: Read-only attribute
3131

packages/pluggableWidgets/image-cropper-web/src/components/ImageCropperContainer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export const ImageCropperContainer = observer(function ImageCropperContainer(
9393
zoom={store.zoom}
9494
minZoom={Number(props.minZoom)}
9595
maxZoom={Number(props.maxZoom)}
96-
setZoom={next => store.setZoom(typeof next === "function" ? next(store.zoom) : next)}
96+
setZoom={next => store.setZoom(next)}
9797
zoomAnchor={store.zoomAnchor}
9898
wheelZoomMode={props.zoomEnabled ? props.wheelZoomMode : "off"}
9999
grayscale={store.grayscale}

packages/pluggableWidgets/image-cropper-web/src/stores/ImageCropperStore.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ValueStatus } from "mendix";
22
import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx";
3+
import { type SetStateAction } from "react";
34
import { type Crop, type PixelCrop } from "react-image-crop";
45
import { DerivedPropsGate, SetupComponent } from "@mendix/widget-plugin-mobx-kit/main";
56
import { debounce } from "@mendix/widget-plugin-platform/utils/debounce";
@@ -261,14 +262,18 @@ export class ImageCropperStore implements SetupComponent {
261262
}
262263
}
263264

264-
setZoom(next: number): void {
265+
setZoom(next: SetStateAction<number>): void {
266+
// Accept the React setState contract (value or updater). The wheel-zoom hook drives
267+
// zoom with setZoom(prev => …), so the store — the owner of `zoom` — resolves the
268+
// updater against its own value instead of leaking that into the container.
269+
const value = typeof next === "function" ? next(this.zoom) : next;
265270
// Freeze the anchor at the current box center. Recomputing it only here (not while the
266271
// box moves) keeps the image stable during drags but still zooms into the box.
267272
const live = this.liveCrop;
268273
if (live && live.unit === "%") {
269274
this.zoomAnchor = { x: (live.x + live.width / 2) / 100, y: (live.y + live.height / 2) / 100 };
270275
}
271-
this.zoom = next;
276+
this.zoom = value;
272277
this.applyDebounced();
273278
}
274279

0 commit comments

Comments
 (0)