Skip to content

[WC-3435]: Image Cropper Design and feats#2280

Open
rahmanunver wants to merge 11 commits into
mainfrom
feat/image-cropper-polish-rotate-bw
Open

[WC-3435]: Image Cropper Design and feats#2280
rahmanunver wants to merge 11 commits into
mainfrom
feat/image-cropper-polish-rotate-bw

Conversation

@rahmanunver

Copy link
Copy Markdown
Contributor

Pull request type

New feature (non-breaking change which adds functionality)


Description

… fix crop alignment

Replace CSS-transform rotation with a pixel re-bake so the crop selection
maps to the visible image, and add a blob-URL live preview so the rotation
is visible before the deferred Mendix commit (Save). Reset now restores the
true first-load original via an internal-change gate in useOriginalImage.
…esign mode

Simplify structure mode to a title row plus a single status/config row
([No attribute selected] vs config summary), dropping the icon and large
image render. Rewrite design mode into a three-state preview driven by the
bound image: static images render the real CropArea (non-interactive) with a
config caption, while dynamic/unbound images show a placeholder glyph with
[No image selected yet].

Extract shared describeConfig/aspectLabel into utils, add the cropper
placeholder asset, declare the png module for TypeScript, and cover both
editor surfaces with new specs.
@rahmanunver rahmanunver requested a review from a team as a code owner June 19, 2026 07:49
@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

AI Code Review

⚠️ Approved with suggestions — low-severity items only, safe to merge


What was reviewed

File Change
src/ImageCropper.editorConfig.ts Structure preview redesigned: new toolbar header, body shows attribute state
src/ImageCropper.editorPreview.tsx Full redesign: real CropArea preview for static images, placeholder glyph otherwise
src/ImageCropper.xml Added studioProCategory/studioCategory, new Editing property group (enableRotation, enableGrayscale, showResetButton)
typings/ImageCropperProps.d.ts New props added matching the XML additions
src/components/CropArea.tsx Added grayscale prop, safeImageUri guard, exports CropAreaProps
src/components/CropToolbar.tsx New toolbar component (rotate, B&W toggle, reset)
src/components/ImageCropperContainer.tsx Wires CropToolbar, handleRotate, handleReset, handleToggleGrayscale, useOriginalImage, usePreviewSrc
src/components/PreviewPane.tsx Added grayscale prop; removed orphaned ctx.save/restore for circle clip
src/hooks/useImageCropperState.ts Added grayscale/setGrayscale state
src/hooks/useOriginalImage.ts New: captures original bytes via fetch for Reset
src/hooks/usePreviewSrc.ts New: local blob URL shown between bake and Mendix commit
src/utils/cropImage.ts Added grayscale option, cleaned up double-rounding
src/utils/rotateImage.ts New: canvas-based 90° rotation with grayscale baking
src/utils/cropMapping.ts New: normalizeRotation / rotatedCanvasSize helpers
src/utils/describeConfig.ts Extracted from editorConfig for reuse
src/utils/safeImageUri.ts New: URI allow-list guard against javascript: / vbscript:
CHANGELOG.md Entry simplified to "Initial release."
src/__tests__/ImageCropper.spec.tsx Added reset tests
src/__tests__/ImageCropperRotation.spec.tsx New integration test suite
src/__tests__/ImageCropper.editor.spec.tsx New editor preview test suite
src/components/__tests__/CropArea.spec.tsx New
src/components/__tests__/CropToolbar.spec.tsx New
src/components/__tests__/PreviewPane.spec.tsx New
src/hooks/__tests__/useImageCropperState.spec.ts New
src/hooks/__tests__/useOriginalImage.spec.ts New
src/hooks/__tests__/usePreviewSrc.spec.ts New
src/utils/__tests__/cropImage.spec.ts Updated + new grayscale test
src/utils/__tests__/cropMapping.spec.ts New
src/utils/__tests__/rotateImage.spec.ts New
src/utils/__tests__/safeImageUri.spec.ts New

Skipped (out of scope): dist/, *.png / *.png binary assets, pnpm-lock.yaml


Findings

⚠️ Low — createRef inside a component body recreates ref on every render

File: src/ImageCropper.editorPreview.tsx line 22
Note: createRef is for class components; inside a function component it allocates a new ref object on every render. This is display-only editor code so it never causes a visible bug here, but it is semantically wrong and will confuse future readers.
Fix:

// replace
const imageRef = createRef<HTMLImageElement>();
// with
const imageRef = useRef<HTMLImageElement>(null);

⚠️ Low — Reset button lacks an aria-label

File: src/components/CropToolbar.tsx line 53
Note: The Rotate left and Rotate right buttons have descriptive aria-label attributes, and the B&W button has both aria-label and aria-pressed. The Reset button is identified only by its visible text — that is fine for most users, but adding a consistent aria-label matches the convention established by the rest of the toolbar and satisfies the icon-button accessibility note.
This is a minor consistency nit; the button is otherwise fully accessible via its visible text.


⚠️ Low — Stale closure risk in usePreviewSrc: state mutation during render

File: src/hooks/usePreviewSrc.ts lines 91–97
Note: Calling setPreviewSrc(undefined) and revoke() synchronously inside the render function body (not inside a useEffect) is a React anti-pattern. It mutates blob state and schedules a re-render during the current render. It works in practice because React re-runs the component body, but the React docs explicitly forbid side-effects during render. The correct pattern is a useEffect or a useLayoutEffect with committedUri as a dependency:

useEffect(() => {
    return () => {
        if (blobRef.current) {
            revoke();
            setPreviewSrc(undefined);
        }
    };
}, [committedUri, revoke]);

Positives

  • safeImageUri is exactly the right defense-in-depth: a narrow allow-list (https?:, blob:, data:image/) with a dedicated unit test covering javascript:, data:text/html, and vbscript: — prevents URI injection before it can reach the DOM.
  • useOriginalImage correctly uses the cleanup cancelled = true guard to prevent state updates from resolved-after-unmount fetches — async-safe by the book.
  • Removing ctx.save/restore around the circle clip path in PreviewPane and cropImage is a correct simplification: clip() doesn't need a save/restore when the canvas state doesn't need to be restored afterward.
  • CropToolbar returns null (not an empty <div>) when all feature flags are off — the component is a true no-op in minimal configs.
  • describeConfig extraction is clean: the function is now shared between editorConfig (structure view) and editorPreview (design view) without duplication, and the exhaustive never check on aspectRatio is preserved.
  • XML property keys are all lowerCamelCase (enableRotation, enableGrayscale, showResetButton) and match the TypeScript interface exactly — no XML/TS alignment drift.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant