MobX crop store for Image Cropper#2321
Conversation
This comment has been minimized.
This comment has been minimized.
…bx-kit Dependencies for the MobX crop store: mobx 6.12.3 and mobx-react-lite 4.0.7 (pinned to match the rest of the monorepo) plus the shared widget-plugin-mobx-kit gate/setup utilities.
Centralize crop-domain working state (live/committed crop, zoom, grayscale, zoom anchor) and its operations in a MobX store. Store methods read observable state directly, so the debounced auto-apply no longer needs a stable-identity callback reading state-mirroring refs. The store owns the debounce + armed/user-interacted apply gate (previously useAutoApplyCrop) and exposes crop operations as actions: setZoom, commitCrop, toggleGrayscale, rotate, reset, initFromImageLoad, onImageChanged. aspect is a computed derived from gated props. Imperative React-owned dependencies (live img element, blob-URL preview, original-image capture, internal-change flag) are injected via setDeps and not owned by the store. Preview generation stays an async action, not a computed, because cropImage/rotateImage are async canvas operations with side effects. Add headless unit tests covering the commit gate, debounce coalescing, no-stale-reads, rotate/reset/grayscale flows, inbound image sync, and read-only/no-crop guards.
Wire ImageCropperContainer to the MobX store via the mobx-kit gate pattern (GateProvider + useConst + useSetup) and wrap it in observer. Crop operations become plain store method calls. Remove the five state-mirroring refs (committedCropRef, zoomRef, grayscaleRef, liveCropRef, zoomAnchorRef) and the two callback mirrors (markInternalRef, showPreviewRef); the store reads its own observable state, so stable-identity closures no longer need them. Config and the controlled image value are read live through the gate; the bound uri is synced inbound via a single effect calling store.onImageChanged. usePreviewSrc, useOriginalImage and the DOM image ref stay in React and are injected into the store through setDeps. Delete useAutoApplyCrop and useImageCropperState, now subsumed by the store.
fb16fae to
edfa936
Compare
This comment has been minimized.
This comment has been minimized.
AI Code Review
What was reviewed
Skipped (out of scope): CI check status could not be retrieved in this environment — verify checks are green before merging. Findings
|
Pull request type
Refactoring (e.g. file rename, variable rename, etc.)
Description
Stacked on top of the Image Cropper PR (
feat/image-cropper-polish-rotate-bw) — base is set to that branch, so this diff shows only the MobX refactor, not the base PR's changes.Why. Review feedback flagged that the widget's state had become too coupled to React.
ImageCropperContainercarried five state-mirroring refs (committedCropRef,zoomRef,grayscaleRef,liveCropRef,zoomAnchorRef) plus two callback mirrors (markInternalRef,showPreviewRef). They existed only becauseapplyCropwas a stable-identityuseCallback(kept stable for the 400ms auto-apply debouncer) and therefore couldn't read current state through its stale closure. A MobX store method readsthis.zoomdirectly, so the refs are no longer needed.Moved into
ImageCropperStore(MobX): the crop-domain working state —liveCrop,committedCrop,zoomAnchor(observable.ref),zoom,grayscale(observable) — plusaspect(computed) and the crop operations as actions (setZoom,commitCrop,toggleGrayscale,rotate,reset,initFromImageLoad,onImageChanged). The store also owns the debounce + auto-apply gate (absorbinguseAutoApplyCrop). Crop ops are now plain calls likestore.setZoom(v)/store.rotate(90).Stayed normal React state (imperative / DOM-bound): the
imageRefDOM node,usePreviewSrc(blob object-URL lifecycle),useOriginalImage(fetch + unmount cleanup + internal-change flag), andCropArea's local DOM-measurement state. These are injected into the store viasetDeps. Config props are read live through the mobx-kit gate, not copied into observables.Controlled/uncontrolled unchanged. The only externally controlled value is
props.image(MendixEditableValue); zoom/rotation/grayscale/crop are internal. Inbound image changes sync through oneuri-keyed effect callingstore.onImageChanged(); outbound writes gomarkInternalChange()→setValue(), with the internal-change flag absorbing the resultingurichange so there's no feedback loop. No reactions used for prop syncing.Preview stays an async action, not a computed —
cropImage/rotateImageare async canvas operations reading a live DOM<img>, so they can't be a pure synchronous computed.Removed: the 5 mirror refs + 2 callback mirrors, and the
useAutoApplyCrop/useImageCropperStatehooks (subsumed by the store).ImageCropperContaineris now anobserverwired via the sharedwidget-plugin-mobx-kitgate pattern (GateProvider+useConst+useSetup), matchingcustom-chart-web.Public API unchanged — no XML/props changes; widget behavior is identical.