-
Notifications
You must be signed in to change notification settings - Fork 31
Feat/image-upload-component #3654
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
📝 WalkthroughWalkthroughAdds a new ImageUpload feature (components, hooks, utils, styles, tests, and CodeGen config), updates Dropzone props/accept handling, extends FileUpload summary recognition, adjusts Card/Cards styling and AppCard props, and adds i18n keys for image upload. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (3 warnings)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…tsimplify mouse dragging and support touch dragging (#3656)
* reuse dropzonecomponent and display only when imgsrc is not defined * bring back the reset button * move controller section to its own file
* move image cropper into card wip * working cropping component in card * conforrm to styling naming in repo * canvas height adjustment --------- Co-authored-by: lassopicasso <[email protected]>
* clean up css and style closer to figma design
…fn) (#3668) * added save and cancel buttons with functionality (some left for save fn) * Adding AttachmentsPlugin to ImageUpload component (it needs to be defined as a form component for this to work, which is more fitting) * save cropped image to backend * feedback * small fix --------- Co-authored-by: Ole Martin Handeland <[email protected]>
* added save and cancel buttons with functionality (some left for save fn) * Adding AttachmentsPlugin to ImageUpload component (it needs to be defined as a form component for this to work, which is more fitting) * save cropped image to backend * redesign slider + buttons --------- Co-authored-by: Ole Martin Handeland <[email protected]>
* wip * revert unneccesary change * clean up
* add hook that handles the saved image and controllers buttons when saved
* support custom config * support mismatch of height and width if type is circle
* remove dropzone icon and refactor css * replace look of placeholder * remove unused css * remove focus for now
* cursor change to grabbing when active in canvas * make change image button support keyboard
* add validationmessages for imageupload size and types * clear validationmessages on cancel
* add language texts and use them throughout the component * change wording slightly and remove unused text * make file-endings the same
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (2)
src/layout/ImageUpload/ImageCanvas.tsx (2)
98-101
: Redraw when image finishes loading to avoid blank canvasCurrent draw runs before image completes; add a load listener.
useEffect(() => { draw(); }, [draw, canvasWidth]); + + // Redraw once the image finishes loading + useEffect(() => { + const img = imageRef.current; + if (!img) return; + const onLoad = () => draw(); + if (!img.complete) { + img.addEventListener('load', onLoad, { once: true }); + } + return () => img.removeEventListener('load', onLoad); + }, [imageRef, draw]);
103-125
: Harden drag: filter pointerId, clamp movement, and ensure cleanupPrevents multi-pointer interference and unconstrained panning; adds better cleanup.
const handlePointerDown = (e: React.PointerEvent<HTMLCanvasElement>) => { e.preventDefault(); const canvas = canvasRef.current; if (!canvas) { return; } canvas.setPointerCapture(e.pointerId); const startDrag = { x: e.clientX - position.x, y: e.clientY - position.y }; + const activePointerId = e.pointerId; - const handlePointerMove = (moveEvent: PointerEvent) => { - onPositionChange({ - x: moveEvent.clientX - startDrag.x, - y: moveEvent.clientY - startDrag.y, - }); - }; - const handlePointerUp = () => { - canvas.releasePointerCapture(e.pointerId); - document.removeEventListener('pointermove', handlePointerMove); - document.removeEventListener('pointerup', handlePointerUp); - }; - document.addEventListener('pointermove', handlePointerMove); - document.addEventListener('pointerup', handlePointerUp); + const handlePointerMove = (moveEvent: PointerEvent) => { + if (moveEvent.pointerId !== activePointerId) return; + const next = { + x: moveEvent.clientX - startDrag.x, + y: moveEvent.clientY - startDrag.y, + }; + const img = imageRef.current; + onPositionChange(img ? constrainToArea({ image: img, zoom, position: next, cropArea }) : next); + }; + const handlePointerUp = (upEvent: PointerEvent) => { + if (upEvent.pointerId !== activePointerId) return; + canvas.releasePointerCapture(activePointerId); + document.removeEventListener('pointermove', handlePointerMove); + }; + document.addEventListener('pointermove', handlePointerMove, { passive: true }); + document.addEventListener('pointerup', handlePointerUp, { once: true }); + canvas.addEventListener('lostpointercapture', handlePointerUp, { once: true }); };
🧹 Nitpick comments (7)
src/layout/ImageUpload/ImageCanvas.tsx (7)
7-7
: Import constrainToArea to enable clamped panningNeeded for both pointer and keyboard panning constraints.
-import { cropAreaPlacement, drawCropArea, imagePlacement } from 'src/layout/ImageUpload/imageUploadUtils'; +import { cropAreaPlacement, drawCropArea, imagePlacement, constrainToArea } from 'src/layout/ImageUpload/imageUploadUtils';If
constrainToArea
isn’t available, I can generate a minimal helper for you.
24-28
: Clamp zoom to sane bounds to avoid negative/oversized scalesPrevents broken rendering and numeric drift.
const CANVAS_HEIGHT = 320; const CANVAS_WIDTH = 800; const MOBILE_CANVAS_WIDTH = 400; const CONTAINER_WIDTH = 450; +const MIN_ZOOM = 0.1; +const MAX_ZOOM = 4;const handleWheel = useCallback( (e: WheelEvent) => { e.preventDefault(); - onZoomChange(zoom - e.deltaY * 0.001); + const next = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom - e.deltaY * 0.001)); + onZoomChange(next); }, [zoom, onZoomChange], );If product requires the image to always cover the crop area, we should compute minZoom dynamically from image/crop sizes. Want me to wire that?
Also applies to: 128-134
147-160
: Constrain keyboard panning to keep crop area coveredReuses the same clamp as pointer drag.
const handleKeyDown = (e: React.KeyboardEvent<HTMLCanvasElement>) => { const moveAmount = 10; - const keyMap: Record<string, () => void> = { - ArrowUp: () => onPositionChange({ ...position, y: position.y - moveAmount }), - ArrowDown: () => onPositionChange({ ...position, y: position.y + moveAmount }), - ArrowLeft: () => onPositionChange({ ...position, x: position.x - moveAmount }), - ArrowRight: () => onPositionChange({ ...position, x: position.x + moveAmount }), - }; + const move = (dx: number, dy: number) => { + const img = imageRef.current; + const next = { x: position.x + dx, y: position.y + dy }; + onPositionChange(img ? constrainToArea({ image: img, zoom, position: next, cropArea }) : next); + }; + const keyMap: Record<string, () => void> = { + ArrowUp: () => move(0, -moveAmount), + ArrowDown: () => move(0, moveAmount), + ArrowLeft: () => move(-moveAmount, 0), + ArrowRight: () => move(moveAmount, 0), + };
172-176
: Spinner a11y: aria-hidden conflicts with aria-labelExpose status to AT; remove aria-hidden and set role/status.
- <Spinner - aria-hidden='true' - data-size='lg' - aria-label={langAsString('general.loading')} - /> + <Spinner + data-size='lg' + role='status' + aria-live='polite' + aria-label={langAsString('general.loading')} + />
183-192
: Canvas a11y: declare role and shortcutsImproves discoverability of keyboard support.
<canvas + role='img' onPointerDown={handlePointerDown} onKeyDown={handleKeyDown} tabIndex={0} ref={canvasRef} height={CANVAS_HEIGHT} width={canvasWidth} className={classes.canvas} aria-label='Image cropping area' + aria-keyshortcuts='ArrowUp,ArrowDown,ArrowLeft,ArrowRight' />
61-72
: Crisp rendering on high‑DPI displaysScale the backing store to devicePixelRatio; keep CSS size unchanged.
const draw = useCallback(() => { const canvas = canvasRef.current; const ctx = canvas?.getContext('2d'); const img = imageRef.current; if (!canvas || !img?.complete || !ctx) { return; } + // High-DPI backing store + const dpr = window.devicePixelRatio || 1; + const targetW = canvasWidth * dpr; + const targetH = CANVAS_HEIGHT * dpr; + if (canvas.width !== targetW || canvas.height !== targetH) { + canvas.width = targetW; + canvas.height = targetH; + } + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.clearRect(0, 0, canvas.width, canvas.height);ref={canvasRef} height={CANVAS_HEIGHT} width={canvasWidth} + style={{ width: `${canvasWidth}px`, height: `${CANVAS_HEIGHT}px` }} className={classes.canvas}
- }, [zoom, position, cropArea, imageRef, canvasRef]); + }, [zoom, position, cropArea, imageRef, canvasRef, canvasWidth]);Also applies to: 183-191, 96-96
49-53
: Avoid non‑null assertion in updateWidthMinor safety/readability improvement.
- const updateWidth = () => { - const width = containerRef.current!.offsetWidth > CONTAINER_WIDTH ? CANVAS_WIDTH : MOBILE_CANVAS_WIDTH; - setCanvasWidth(width); - }; + const updateWidth = () => { + const el = containerRef.current; + if (!el) return; + const width = el.offsetWidth > CONTAINER_WIDTH ? CANVAS_WIDTH : MOBILE_CANVAS_WIDTH; + setCanvasWidth(width); + };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/layout/ImageUpload/ImageCanvas.tsx
(1 hunks)src/layout/ImageUpload/ImageDropzone.module.css
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/layout/ImageUpload/ImageDropzone.module.css
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/layout/ImageUpload/ImageCanvas.tsx
🧬 Code graph analysis (1)
src/layout/ImageUpload/ImageCanvas.tsx (3)
src/layout/ImageUpload/imageUploadUtils.ts (5)
Position
(1-1)CropArea
(10-10)imagePlacement
(56-63)cropAreaPlacement
(68-72)drawCropArea
(81-89)src/layout/ImageUpload/useImageFile.tsx (1)
useImageFile
(17-52)src/features/language/useLanguage.ts (1)
useLanguage
(90-93)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/layout/ImageUpload/ImageDropzone.tsx (1)
46-51
: Make “Find” text activate the file input.Wrap it in a label linked to the input id to improve keyboard/a11y.
- <span className={classes.blueUnderLine}> - {' '} - <Lang id='form_filler.file_uploader_find' /> - </span> + <label + className={classes.blueUnderLine} + htmlFor={componentId} + > + {' '} + <Lang id='form_filler.file_uploader_find' /> + </label>
🧹 Nitpick comments (2)
src/layout/ImageUpload/ImageDropzone.tsx (2)
41-45
: Minor semantics: prefer strong over b.Use strong for emphasis; b is purely presentational.
- <b id={dragLabelId}> + <strong id={dragLabelId}> @@ - </b> + </strong>
55-56
: Optional: clarify valid types in text.Consider passing customTextParameters to include extensions (e.g., “.jpg, .png, .gif”) for clearer guidance.
Example:
<Lang id='image_upload_component.valid_file_types' customTextParameters={{ types: VALID_FILE_ENDINGS.join(', ') }} />
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/app-components/Dropzone/Dropzone.tsx
(4 hunks)src/layout/ImageUpload/ImageDropzone.tsx
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/app-components/Dropzone/Dropzone.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/layout/ImageUpload/ImageDropzone.tsx
🧬 Code graph analysis (1)
src/layout/ImageUpload/ImageDropzone.tsx (4)
src/app-components/Dropzone/Dropzone.tsx (2)
IDropzoneProps
(16-29)Dropzone
(33-103)src/hooks/useDeviceWidths.ts (1)
useIsMobileOrTablet
(33-35)src/layout/ImageUpload/imageUploadUtils.ts (1)
VALID_FILE_ENDINGS
(7-7)src/features/language/Lang.tsx (1)
Lang
(15-23)
🔇 Additional comments (1)
src/layout/ImageUpload/ImageDropzone.tsx (1)
30-34
: Good: unique id and default click behavior preserved.Using componentId avoids collisions, and not overriding onClick keeps the file picker on click.
/publish |
PR release:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (10)
src/language/texts/nb.ts (1)
205-214
: Text polish: align indefinites, spacing, and list phrasing (minor).Small Bokmål consistency nits and UX clarity. Also consider parameterizing size like other messages.
Apply minimal fixes:
- 'image_upload_component.button_delete': 'Slett bildet', + 'image_upload_component.button_delete': 'Slett bilde', - 'image_upload_component.slider_zoom': 'Tilpass bildet', + 'image_upload_component.slider_zoom': 'Tilpass bilde', - 'Feil filformat. Last opp en fil med bildeformat (jpg, jpeg, png, gif).', + 'Feil filformat. Last opp en fil i et av følgende bildeformater: jpg, jpeg, png og gif.', - 'image_upload_component.error_file_size_exceeded': 'Filen er for stor. Største tillatte filstørrelse er 10MB.', + 'image_upload_component.error_file_size_exceeded': 'Filen er for stor. Største tillatte filstørrelse er 10 MB.',Optional (if UI can supply the limit): parameterize max size to match existing patterns like
form_filler.file_uploader_max_size_mb
:- 'image_upload_component.error_file_size_exceeded': 'Filen er for stor. Største tillatte filstørrelse er 10 MB.', + 'image_upload_component.error_file_size_exceeded': 'Filen er for stor. Største tillatte filstørrelse er {0} MB.',Side note: You may reuse existing generic texts (e.g.,
'form_filler.file_uploader_valid_file_format'
) to avoid divergence between file/image upload messages.test/e2e/integration/component-library/image-upload.ts (6)
34-37
: Reduce flakiness: assert saved image before asserting canvas removalWait for the saved image to appear first; then assert the cropping canvas is gone.
- cy.findByRole('button', { name: /Lagre/i }).click(); - cy.get('canvas').should('not.exist'); - cy.findByRole('img', { name: /uploadThis1.jpg/ }).should('be.visible'); + cy.findByRole('button', { name: /Lagre/i }).click(); + cy.findByRole('img', { name: /uploadThis1.png/i }).should('be.visible'); + cy.get('canvas').should('not.exist');
36-36
: Update assertion to match corrected filename- cy.findByRole('img', { name: /uploadThis1.jpg/ }).should('be.visible'); + cy.findByRole('img', { name: /uploadThis1.png/i }).should('be.visible');
46-46
: Use accessible, specific selector for the saved imageAvoid broad 'img' selector; assert by accessible name.
- cy.get('img').should('be.visible'); + cy.findByRole('img', { name: /uploadThis1.png/i }).should('be.visible');
48-48
: Update deletion assertion to match corrected filename- cy.findByRole('img', { name: /uploadThis1.jpg/ }).should('not.exist'); + cy.findByRole('img', { name: /uploadThis1.png/i }).should('not.exist');
26-26
: Normalize data attribute casing for consistencyUse lowercase attribute name everywhere to match the rest of the suite.
- cy.get('[data-componentId="ImageUploadPage-ImageUpload"]').should('be.visible'); + cy.get('[data-componentid="ImageUploadPage-ImageUpload"]').should('be.visible');Also applies to: 50-50
14-15
: Consider de-coupling tests from locale-specific labelsSearch shows many hard-coded Norwegian UI texts across e2e tests, making them brittle to locale/text changes — prefer stable selectors (data-testid), route-based navigation, or centralized pageobject selectors instead of UI text.
- Examples to update: test/e2e/integration/component-library/image-upload.ts (cy.gotoNavPage('Bildeopplasting') + 'Avbryt'/'Lagre'/'Slett bildet'), test/e2e/support/formFiller.ts (findByRole button 'Lagre'), test/e2e/pageobjects/app-frontend.ts (button:contains("Print / Lagre PDF")), test/e2e/integration/frontend-test/components.ts and group.ts (many 'Avbryt' / 'Lagre og lukk' / 'Slett' uses), plus multiple component-library tests (repeating-group, attachments-in-group, multiple-select, list, checkboxes).
Action: replace UI-text-based selectors with data-testid or route/pageobject selectors and centralize reusable selectors in pageobjects.
test/e2e/support/apps/component-library/uploadImageAndVerify.ts (3)
9-13
: Normalize selector casing and scope the file input to the componentAvoid repeating the long selector and keep attribute casing consistent.
-export const uploadImageAndVerify = (fileName: string) => { - cy.get('[data-componentId="ImageUploadPage-ImageUpload"]').should('be.visible'); - - cy.get('[data-componentId="ImageUploadPage-ImageUpload"]') - .find('input[type="file"]') - .selectFile(makeTestFile(fileName), { force: true }); +export const uploadImageAndVerify = (fileName: string) => { + const componentSel = '[data-componentid="ImageUploadPage-ImageUpload"]'; + cy.get(componentSel).should('be.visible'); + cy.get(componentSel) + .find('input[type="file"]') + .selectFile(makeTestFile(fileName), { force: true });
16-27
: Avoid 'as' cast; add null-guard; more robust pixel checkUse Cypress generics to type the element, guard context, and check alpha channel for any drawn pixel.
- cy.get('canvas').should('be.visible'); - cy.get('canvas').then(($canvas) => { - const canvas = $canvas[0] as HTMLCanvasElement; - const ctx = canvas.getContext('2d'); - const data = ctx?.getImageData(0, 0, canvas.width, canvas.height).data; - // Count non-transparent pixels - const hasImage = - data && - Array.from(data).some( - (value, index) => index % 4 !== 3 && value !== 0, // not alpha channel, and not 0 - ); - expect(hasImage).to.be.true; - }); + cy.get<HTMLCanvasElement>('canvas').should('be.visible'); + cy.get<HTMLCanvasElement>('canvas').then(($canvas) => { + const canvas = $canvas[0]; + const ctx = canvas.getContext('2d'); + expect(ctx, '2D context').to.not.be.null; + if (!ctx) return; + const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height); + let hasOpaquePixel = false; + for (let i = 3; i < data.length; i += 4) { + if (data[i] > 0) { + hasOpaquePixel = true; + break; + } + } + expect(hasOpaquePixel, 'canvas contains drawn image data').to.be.true; + });
8-13
: Export a shared selector constant for the ImageUpload componentDefine and export the selector and replace all hard-coded occurrences with it. Use the exact casing used in the code:
export const IMAGE_UPLOAD_COMPONENT = '[data-componentId="ImageUploadPage-ImageUpload"]';
Replace cy.get('[data-componentId="ImageUploadPage-ImageUpload"]') with cy.get(IMAGE_UPLOAD_COMPONENT) in:
- test/e2e/support/apps/component-library/uploadImageAndVerify.ts (lines ~9,11)
- test/e2e/integration/component-library/image-upload.ts (lines ~26,50)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
src/language/texts/en.ts
(1 hunks)src/language/texts/nb.ts
(1 hunks)src/language/texts/nn.ts
(1 hunks)src/layout/ImageUpload/ImageCanvas.module.css
(1 hunks)src/layout/ImageUpload/ImageControllers.tsx
(1 hunks)test/e2e/integration/component-library/image-upload.ts
(1 hunks)test/e2e/support/apps/component-library/uploadImageAndVerify.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- src/language/texts/nn.ts
- src/layout/ImageUpload/ImageCanvas.module.css
- src/language/texts/en.ts
- src/layout/ImageUpload/ImageControllers.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
test/e2e/integration/component-library/image-upload.ts
src/language/texts/nb.ts
test/e2e/support/apps/component-library/uploadImageAndVerify.ts
🧬 Code graph analysis (1)
test/e2e/integration/component-library/image-upload.ts (2)
test/e2e/pageobjects/app-frontend.ts (1)
AppFrontend
(3-371)test/e2e/support/apps/component-library/uploadImageAndVerify.ts (1)
uploadImageAndVerify
(8-28)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Type-checks, eslint, unit tests and SonarCloud
- GitHub Check: Install
🔇 Additional comments (2)
src/language/texts/nb.ts (1)
205-214
: Approve — locale keys are in parity and used.
- en/nb/nn each contain 448 keys; no key diffs found between nb↔en and nb↔nn.
- All image_upload_component keys exist in en/nb/nn and are referenced from src/layout/ImageUpload/{ImageDropzone.tsx, imageUploadUtils.ts, ImageControllers.tsx}.
- nb.ts and nn.ts use
satisfies FixedLanguageList
; en.ts does not — optional: addsatisfies FixedLanguageList
to en.ts for consistency.test/e2e/integration/component-library/image-upload.ts (1)
8-52
: Solid scenario coverageHappy path, cancel, save, and delete flows are all covered and use accessible roles. Once the filename/mime fixes land, this suite looks good.
test/e2e/support/apps/component-library/uploadImageAndVerify.ts
Outdated
Show resolved
Hide resolved
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* support summary2 * small adjustments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (5)
src/layout/ImageUpload/imageUploadUtils.ts (4)
36-47
: Use natural image dimensions; guard zero to avoid bad clamping.CSS
image.width/height
can differ from pixels; zero gives degenerate clamps. UsenaturalWidth/Height
with a >=1 floor.Apply:
export function constrainToArea({ image, zoom, position, cropArea }: ConstrainToAreaParams): Position { - const scaledWidth = image.width * zoom; - const scaledHeight = image.height * zoom; + const iw = image.naturalWidth || image.width || 0; + const ih = image.naturalHeight || image.height || 0; + const scaledWidth = Math.max(1, iw) * zoom; + const scaledHeight = Math.max(1, ih) * zoom;
56-63
: Same: natural dimensions for placement.Apply:
export const imagePlacement = ({ canvas, img, zoom, position }: ImagePlacementParams) => { - const scaledWidth = img.width * zoom; - const scaledHeight = img.height * zoom; + const iw = img.naturalWidth || img.width || 0; + const ih = img.naturalHeight || img.height || 0; + const scaledWidth = Math.max(1, iw) * zoom; + const scaledHeight = Math.max(1, ih) * zoom;
100-117
: Zoom math can produce NaN/∞; add validation and clamping.If min/max/value ≤ 0 or min===max, logs break; normalized range should be [0,100].
Apply:
-function getLogValues({ minZoom, maxZoom }: ZoomParams): { logScale: number; logMin: number } { - const logMin = Math.log(minZoom); - const logMax = Math.log(maxZoom); - return { logScale: (logMax - logMin) / 100, logMin }; -} +function getLogValues({ minZoom, maxZoom }: ZoomParams): { logScale: number; logMin: number } { + if (minZoom <= 0 || maxZoom <= 0) { + throw new Error('minZoom and maxZoom must be > 0'); + } + const logMin = Math.log(minZoom); + const logMax = Math.log(maxZoom); + return { logScale: (logMax - logMin) / 100, logMin }; +} @@ export function logToNormalZoom({ value, minZoom, maxZoom }: CalculateZoomParams): number { const { logScale, logMin } = getLogValues({ minZoom, maxZoom }); if (logScale === 0) { return 0; } // Avoid division by zero if minZoom equals maxZoom - return (Math.log(value) - logMin) / logScale; + const safe = Math.max(value, Number.EPSILON); + const normalized = (Math.log(safe) - logMin) / logScale; + return Math.min(100, Math.max(0, normalized)); }
119-122
: calculateMinZoom should use natural dimensions and guard zero.Apply:
-export const calculateMinZoom = ({ img, cropArea }: CalculateMinZoomParams) => - Math.max(cropArea.width / img.width, cropArea.height / img.height); +export const calculateMinZoom = ({ img, cropArea }: CalculateMinZoomParams) => { + const iw = img.naturalWidth || img.width || 0; + const ih = img.naturalHeight || img.height || 0; + if (iw <= 0 || ih <= 0) return 1; + return Math.max(cropArea.width / iw, cropArea.height / ih); +};src/layout/ImageUpload/index.tsx (1)
38-40
: renderSummary throws; crashes summary rendering. Return null or implement.Apply:
- renderSummary(_props: SummaryRendererProps): JSX.Element | null { - throw new Error('Method not implemented.'); // TODO - } + renderSummary(_props: SummaryRendererProps): JSX.Element | null { + return null; // TODO: implement legacy summary + }
🧹 Nitpick comments (4)
src/layout/ImageUpload/imageUploadUtils.ts (2)
12-27
: Validate and normalize crop area dimensions (avoid zero/negative).Width/height can be zero/negative, leading to broken math later. Clamp to at least 1.
Apply:
export const getCropArea = (cropArea?: CropAreaParams): CropArea => { const defaultSize = 250; - let width = cropArea?.width ?? defaultSize; - let height = cropArea?.height ?? defaultSize; + let width = cropArea?.width ?? defaultSize; + let height = cropArea?.height ?? defaultSize; + width = Math.max(1, Math.floor(width)); + height = Math.max(1, Math.floor(height));
81-89
: Guard drawCropArea against invalid sizes.No-op or throw on non-positive sizes to avoid confusing canvas state.
Apply:
export function drawCropArea({ ctx, x = 0, y = 0, cropArea }: DrawCropAreaParams) { const { width, height, type } = cropArea; + if (width <= 0 || height <= 0) { + return; + } ctx.beginPath();src/layout/ImageUpload/ImageUploadSummary2.tsx (1)
16-24
: Nit: pluralize variable for clarity.Rename
attachment
toattachments
to match array usage and improve readability.-export function ImageUploadSummary2({ targetBaseComponentId }: Summary2Props) { - const attachment = useUploaderSummaryData(targetBaseComponentId); +export function ImageUploadSummary2({ targetBaseComponentId }: Summary2Props) { + const attachments = useUploaderSummaryData(targetBaseComponentId); @@ - const isEmpty = attachment.length === 0; + const isEmpty = attachments.length === 0; @@ - <FileTable + <FileTable baseComponentId={targetBaseComponentId} mobileView={isSmall} - attachments={attachment} + attachments={attachments} options={options} isSummary={true} isFetching={isFetching} />Also applies to: 42-56
src/layout/ImageUpload/index.tsx (1)
16-19
: Display data should include only uploaded files.Avoid listing temp/failed items; filter by uploaded state.
Apply:
useDisplayData(baseComponentId: string): string { const attachments = useAttachmentsFor(baseComponentId); - return attachments.map((a) => a.data.filename).join(', '); + return attachments + .filter((a) => a.data.uploaded) + .map((a) => a.data.filename) + .join(', '); }If
isAttachmentUploaded(a)
utility exists elsewhere, prefer it.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/layout/FileUpload/FileUploadTable/FileTable.tsx
(1 hunks)src/layout/ImageUpload/ImageDropzone.tsx
(1 hunks)src/layout/ImageUpload/ImageUploadComponent.tsx
(1 hunks)src/layout/ImageUpload/ImageUploadSummary2.tsx
(1 hunks)src/layout/ImageUpload/imageUploadUtils.ts
(1 hunks)src/layout/ImageUpload/index.tsx
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/layout/ImageUpload/ImageUploadComponent.tsx
- src/layout/ImageUpload/ImageDropzone.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/layout/ImageUpload/ImageUploadSummary2.tsx
src/layout/FileUpload/FileUploadTable/FileTable.tsx
src/layout/ImageUpload/imageUploadUtils.ts
src/layout/ImageUpload/index.tsx
src/layout/*/{config.ts,Component.tsx,index.tsx,config.generated.ts}
📄 CodeRabbit inference engine (CLAUDE.md)
Layout components must follow the standardized structure:
config.ts
,Component.tsx
,index.tsx
, and include generated types inconfig.generated.ts
Files:
src/layout/ImageUpload/index.tsx
🧬 Code graph analysis (3)
src/layout/ImageUpload/ImageUploadSummary2.tsx (8)
src/layout/Summary2/SummaryComponent2/types.ts (1)
Summary2Props
(1-3)src/layout/FileUpload/Summary/summary.ts (1)
useUploaderSummaryData
(4-6)src/utils/layout/useNodeItem.ts (1)
useItemWhenType
(15-33)src/features/options/useOptionsFor.ts (1)
useOptionsFor
(11-19)src/hooks/useDeviceWidths.ts (1)
useIsMobileOrTablet
(33-35)src/features/pdf/PDFWrapper.tsx (1)
usePdfModeActive
(11-15)src/features/language/Lang.tsx (1)
Lang
(15-23)src/layout/FileUpload/FileUploadTable/FileTable.tsx (1)
FileTable
(25-146)
src/layout/FileUpload/FileUploadTable/FileTable.tsx (1)
src/utils/layout/useNodeItem.ts (1)
useItemWhenType
(15-33)
src/layout/ImageUpload/index.tsx (9)
src/features/attachments/hooks.ts (1)
useAttachmentsFor
(11-14)src/layout/index.ts (1)
PropsFromGenericComponent
(28-32)src/layout/ImageUpload/ImageUploadComponent.tsx (1)
ImageUploadComponent
(12-36)src/features/form/layout/makeLayoutLookups.ts (1)
LayoutLookups
(66-66)src/layout/layout.ts (1)
IDataModelBindings
(61-64)src/layout/FileUpload/utils/useFileUploaderDataBindingsValidation.ts (1)
useFileUploaderDataBindingsValidation
(10-41)src/layout/LayoutComponent.tsx (2)
SummaryRendererProps
(174-179)ExprResolver
(41-53)src/layout/Summary2/SummaryComponent2/types.ts (1)
Summary2Props
(1-3)src/layout/ImageUpload/ImageUploadSummary2.tsx (1)
ImageUploadSummary2
(16-60)
🔇 Additional comments (1)
src/layout/ImageUpload/index.tsx (1)
34-36
: Action required: confirm useFileUploaderDataBindingsValidation accepts 'ImageUpload'rg returned no matches for the hook declaration; verify whether useFileUploaderDataBindingsValidation's generic includes 'ImageUpload' — if not, update its declaration to:
-export function useFileUploaderDataBindingsValidation<T extends 'FileUpload' | 'FileUploadWithTag'>( +export function useFileUploaderDataBindingsValidation<T extends 'FileUpload' | 'FileUploadWithTag' | 'ImageUpload'>( baseComponentId: string, bindings: IDataModelBindings<T>, ): string[]Call site: src/layout/ImageUpload/index.tsx (lines 34–36)
Run locally to locate the declaration:
rg -n --hidden -S "useFileUploaderDataBindingsValidation" -g '!/node_modules/'
const { textResourceBindings, type, readOnly } = useItemWhenType<'FileUpload' | 'FileUploadWithTag' | 'ImageUpload'>( | ||
baseComponentId, | ||
(t) => t === 'FileUpload' || t === 'FileUploadWithTag', | ||
(t) => t === 'FileUpload' || t === 'FileUploadWithTag' || t === 'ImageUpload', | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
ImageUpload support in FileTable looks correct; verify downstream assumptions.
Union type/predicate update is fine. Confirm that FileTableRow and related render paths don’t assume tags/options for ImageUpload (label() safely returns undefined).
Run to spot any FileTableRow logic keyed only to 'FileUpload'/'FileUploadWithTag':
🏁 Script executed:
#!/bin/bash
rg -n -C2 -e "FileTableRow" -e "tagLabel" -e "FileUploadWithTag" --type=ts --type=tsx
Length of output: 27
🏁 Script executed:
#!/bin/bash
# Search for FileTableRow / tag-related logic and show the FileTable file
echo "=== src/layout/FileUpload/FileUploadTable/FileTable.tsx (first 400 lines) ==="
sed -n '1,400p' src/layout/FileUpload/FileUploadTable/FileTable.tsx || true
echo
echo "=== Search: FileTableRow occurrences ==="
rg -n -C2 "FileTableRow" -g "**/*.{ts,tsx,js,jsx}" || true
echo
echo "=== Search: useItemWhenType occurrences ==="
rg -n -C2 "useItemWhenType" -g "**/*.{ts,tsx,js,jsx}" || true
echo
echo "=== Search: FileUploadWithTag occurrences ==="
rg -n -C2 "FileUploadWithTag" -g "**/*.{ts,tsx,js,jsx}" || true
echo
echo "=== Search: ImageUpload occurrences ==="
rg -n -C2 "ImageUpload" -g "**/*.{ts,tsx,js,jsx}" || true
echo
echo "=== Search: tagLabel or tag-related label() usage ==="
rg -n -C2 "tagLabel" -g "**/*.{ts,tsx,js,jsx}" || true
rg -n -C2 "label\(" -g "**/*.{ts,tsx,js,jsx}" | rg -n "tag" || true
Length of output: 197965
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== src/utils/layout/useNodeItem.ts ==="
sed -n '1,240p' src/utils/layout/useNodeItem.ts || true
echo
echo "=== src/layout/FileUpload/FileUploadTable/FileTableButtons.tsx ==="
sed -n '1,260p' src/layout/FileUpload/FileUploadTable/FileTableButtons.tsx || true
echo
echo "=== src/layout/FileUpload/FileUploadTable/FileTableRow.tsx ==="
sed -n '1,260p' src/layout/FileUpload/FileUploadTable/FileTableRow.tsx || true
Length of output: 17156
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Full src/layout/FileUpload/FileUploadTable/FileTableRow.tsx ==="
sed -n '1,999p' src/layout/FileUpload/FileUploadTable/FileTableRow.tsx || true
echo
echo "=== grep: FileTableButtons usage in this file ==="
rg -n "FileTableButtons" src/layout/FileUpload/FileUploadTable/FileTableRow.tsx || true
echo
echo "=== grep: ButtonCellContent usage in file ==="
rg -n "ButtonCellContent" src/layout/FileUpload/FileUploadTable/FileTableRow.tsx || true
Length of output: 8218
ImageUpload added to FileTable — runtime error possible; fix required
- FileTable now accepts ImageUpload (src/layout/FileUpload/FileUploadTable/FileTable.tsx:33–36). FileTableRow renders FileTableButtons unconditionally (call in src/layout/FileUpload/FileUploadTable/FileTableRow.tsx), but FileTableButtons calls useItemWhenType<'FileUpload' | 'FileUploadWithTag'> and will throw for ImageUpload (src/layout/FileUpload/FileUploadTable/FileTableButtons.tsx:32–36).
- label(...) is safe — it returns undefined when no options are present.
Action: either widen FileTableButtons' useItemWhenType to include 'ImageUpload' or avoid rendering FileTableButtons for ImageUpload (change points above).
🤖 Prompt for AI Agents
In src/layout/FileUpload/FileUploadTable/FileTable.tsx around lines 33–36 you
added ImageUpload to the useItemWhenType call but FileTableRow renders
FileTableButtons unconditionally and FileTableButtons uses
useItemWhenType<'FileUpload' | 'FileUploadWithTag'> which will throw for
ImageUpload; fix by either expanding the type union in
src/layout/FileUpload/FileUploadTable/FileTableButtons.tsx to include
'ImageUpload' (so useItemWhenType<'FileUpload' | 'FileUploadWithTag' |
'ImageUpload'>) or update the render in
src/layout/FileUpload/FileUploadTable/FileTableRow.tsx to only render
<FileTableButtons /> when the resolved type is 'FileUpload' or
'FileUploadWithTag'; implement one of these two changes consistently and add a
comment noting why ImageUpload is handled/filtered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (6)
src/layout/ImageUpload/ImageDropzone.tsx (2)
28-41
: Fix ARIA: move heading to aria-labelledby, keep describedBy for descriptions only.Pass the drag label id via labelId and remove it from describedBy. This improves SR semantics and avoids double announcement.
- const ariaDescribedBy = [descriptionId, dragLabelId, formatLabelId].filter(Boolean).join(' '); + const ariaDescribedBy = [descriptionId, formatLabelId].filter(Boolean).join(' ') || undefined; @@ <Dropzone id={baseComponentId} readOnly={readOnly} onDrop={onDrop} onDragActiveChange={setDragActive} hasValidationMessages={hasErrors} acceptedFiles={acceptedFiles.dropzone} + labelId={dragLabelId} data-color='neutral' className={cn(classes.placeholder, { [classes.dragActive]: dragActive })} describedBy={ariaDescribedBy}
49-52
: Make “find” text activate the file input.Use a label tied to the input id so it’s clickable and keyboard accessible.
- <span className={classes.blueUnderLine}> - {' '} - <Lang id='form_filler.file_uploader_find' /> - </span> + <label + className={classes.blueUnderLine} + htmlFor={baseComponentId} + > + {' '} + <Lang id='form_filler.file_uploader_find' /> + </label>src/layout/FileUpload/FileUploadComponent.tsx (3)
81-85
: ARIA: don’t place the heading in describedBy; reserve for descriptions.Move dragLabelId to aria-labelledby and keep describedBy for description/format text.
- const ariaDescribedBy = [descriptionId, dragLabelId, formatLabelId].filter(Boolean).join(' '); + const ariaDescribedBy = [descriptionId, formatLabelId].filter(Boolean).join(' ') || undefined;
134-147
: Connect both the main label and the in-zone heading via aria-labelledby.Also, avoid blocking click-to-upload unless there’s a UX reason.
<Dropzone id={id} maxFileSize={{ sizeInMB: maxFileSizeInMB, text: langAsString('form_filler.file_uploader_max_size_mb', [maxFileSizeInMB]), }} readOnly={!!readOnly} - onClick={(e) => e.preventDefault()} onDrop={handleDrop} hasValidationMessages={hasValidationErrors(validations)} acceptedFiles={filesToAccept} - labelId={textResourceBindings?.title ? getLabelId(id) : undefined} + labelId={[textResourceBindings?.title ? getLabelId(id) : undefined, dragLabelId] + .filter(Boolean) + .join(' ') || undefined} describedBy={ariaDescribedBy}If the dropzone should not open the file picker on click, keep onClick; otherwise, consider removing it as above. Please confirm intended UX.
159-164
: Make “find” text activate the file input.Use a label tied to the input id to improve click/keyboard accessibility.
- <span className={cn(classes.blueUnderLine)}> - {' '} - <Lang id='form_filler.file_uploader_find' /> - </span> + <label + className={cn(classes.blueUnderLine)} + htmlFor={id} + > + {' '} + <Lang id='form_filler.file_uploader_find' /> + </label>src/layout/ImageUpload/ImageCropper.tsx (1)
161-166
: Initial zoom should respect min zoom (don’t force >= 1).If minZoom < 1 (small images), forcing 1 prevents fitting. Initialize to minZoom or 1, whichever is greater only if UX requires it.
- const updateImageState = ({ minZoom = minAllowedZoom, img = imageRef.current }: UpdateImageState) => { - setZoom(Math.max(1, minZoom)); + const updateImageState = ({ minZoom = minAllowedZoom, img = imageRef.current }: UpdateImageState) => { + setZoom(minZoom); setPosition({ x: 0, y: 0 }); imageRef.current = img; };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
src/app-components/Dropzone/Dropzone.tsx
(3 hunks)src/language/texts/en.ts
(1 hunks)src/language/texts/nb.ts
(1 hunks)src/language/texts/nn.ts
(1 hunks)src/layout/FileUpload/FileUploadComponent.tsx
(3 hunks)src/layout/ImageUpload/ImageControllers.tsx
(1 hunks)src/layout/ImageUpload/ImageCropper.tsx
(1 hunks)src/layout/ImageUpload/ImageDropzone.tsx
(1 hunks)src/layout/ImageUpload/ImageUploadComponent.tsx
(1 hunks)src/layout/ImageUpload/config.ts
(1 hunks)src/layout/ImageUpload/imageUploadUtils.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- src/language/texts/nb.ts
- src/layout/ImageUpload/config.ts
- src/layout/ImageUpload/ImageUploadComponent.tsx
- src/language/texts/nn.ts
- src/language/texts/en.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/layout/ImageUpload/ImageCropper.tsx
src/layout/FileUpload/FileUploadComponent.tsx
src/layout/ImageUpload/ImageControllers.tsx
src/layout/ImageUpload/imageUploadUtils.ts
src/layout/ImageUpload/ImageDropzone.tsx
src/app-components/Dropzone/Dropzone.tsx
🧬 Code graph analysis (5)
src/layout/ImageUpload/ImageCropper.tsx (5)
src/layout/ImageUpload/imageUploadUtils.ts (10)
CropArea
(10-10)getAcceptedFiles
(159-171)Position
(3-3)ValidationErrors
(124-124)calculateMinZoom
(120-121)constrainToArea
(36-47)imagePlacement
(56-63)validateFile
(125-152)cropAreaPlacement
(68-72)drawCropArea
(81-89)src/layout/ImageUpload/useImageFile.tsx (1)
useImageFile
(17-52)src/layout/ImageUpload/ImageCanvas.tsx (1)
ImageCanvas
(29-194)src/layout/ImageUpload/ImageControllers.tsx (1)
ImageControllers
(26-152)src/features/language/Lang.tsx (1)
Lang
(15-23)
src/layout/FileUpload/FileUploadComponent.tsx (1)
src/app-components/Dropzone/mapExtensionToAcceptMime.ts (1)
mapExtensionToAcceptMime
(11-36)
src/layout/ImageUpload/ImageControllers.tsx (4)
src/features/attachments/index.ts (1)
UploadedAttachment
(20-20)src/layout/ImageUpload/imageUploadUtils.ts (3)
AcceptedFiles
(154-157)normalToLogZoom
(106-109)logToNormalZoom
(111-117)src/features/language/useLanguage.ts (1)
useLanguage
(90-93)src/features/language/Lang.tsx (1)
Lang
(15-23)
src/layout/ImageUpload/imageUploadUtils.ts (1)
src/app-components/Dropzone/mapExtensionToAcceptMime.ts (1)
mapExtensionToAcceptMime
(11-36)
src/layout/ImageUpload/ImageDropzone.tsx (6)
src/layout/ImageUpload/imageUploadUtils.ts (1)
AcceptedFiles
(154-157)src/app-components/Dropzone/Dropzone.tsx (2)
IDropzoneProps
(15-27)Dropzone
(31-99)src/utils/layout/useNodeItem.ts (1)
useItemWhenType
(15-33)src/hooks/useDeviceWidths.ts (1)
useIsMobileOrTablet
(33-35)src/components/label/Label.tsx (1)
getDescriptionId
(135-141)src/features/language/Lang.tsx (1)
Lang
(15-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Install
- GitHub Check: Analyze (javascript)
- GitHub Check: Type-checks, eslint, unit tests and SonarCloud
🔇 Additional comments (10)
src/layout/ImageUpload/imageUploadUtils.ts (4)
36-47
: Use naturalWidth/Height and guard zero to avoid NaN/∞ and incorrect scaling.Canvas math should use intrinsic image size and avoid 0-dimension math.
export function constrainToArea({ image, zoom, position, cropArea }: ConstrainToAreaParams): Position { - const scaledWidth = image.width * zoom; - const scaledHeight = image.height * zoom; + const iw = image.naturalWidth || image.width || 0; + const ih = image.naturalHeight || image.height || 0; + const scaledWidth = Math.max(1, iw) * zoom; + const scaledHeight = Math.max(1, ih) * zoom;
56-63
: Same intrinsic-size/zero-guard fix for placement calculations.export const imagePlacement = ({ canvas, img, zoom, position }: ImagePlacementParams) => { - const scaledWidth = img.width * zoom; - const scaledHeight = img.height * zoom; + const iw = img.naturalWidth || img.width || 0; + const ih = img.naturalHeight || img.height || 0; + const scaledWidth = Math.max(1, iw) * zoom; + const scaledHeight = Math.max(1, ih) * zoom;
100-117
: Harden log zoom math (non‑positive values yield NaN/∞).Validate inputs, guard logs, and clamp outputs.
-function getLogValues({ minZoom, maxZoom }: ZoomParams): { logScale: number; logMin: number } { - const logMin = Math.log(minZoom); - const logMax = Math.log(maxZoom); - return { logScale: (logMax - logMin) / 100, logMin }; -} +function getLogValues({ minZoom, maxZoom }: ZoomParams): { logScale: number; logMin: number } { + const safeMin = Math.max(Number.EPSILON, minZoom); + const safeMax = Math.max(safeMin, maxZoom); + const logMin = Math.log(safeMin); + const logMax = Math.log(safeMax); + return { logScale: (logMax - logMin) / 100, logMin }; +} @@ export function normalToLogZoom({ value, minZoom, maxZoom }: CalculateZoomParams): number { const { logScale, logMin } = getLogValues({ minZoom, maxZoom }); - return Math.exp(logMin + logScale * value); + const raw = Math.exp(logMin + logScale * value); + return Math.min(maxZoom, Math.max(minZoom, raw)); } @@ export function logToNormalZoom({ value, minZoom, maxZoom }: CalculateZoomParams): number { const { logScale, logMin } = getLogValues({ minZoom, maxZoom }); if (logScale === 0) { return 0; } - return (Math.log(value) - logMin) / logScale; + const safe = Math.max(Number.EPSILON, value); + const normalized = (Math.log(safe) - logMin) / logScale; + return Math.min(100, Math.max(0, normalized)); }
119-122
: calculateMinZoom should use intrinsic dimensions and avoid division by zero.-export const calculateMinZoom = ({ img, cropArea }: CalculateMinZoomParams) => - Math.max(cropArea.width / img.width, cropArea.height / img.height); +export const calculateMinZoom = ({ img, cropArea }: CalculateMinZoomParams) => { + const iw = img.naturalWidth || img.width || 0; + const ih = img.naturalHeight || img.height || 0; + if (iw <= 0 || ih <= 0) return 1; + return Math.max(cropArea.width / iw, cropArea.height / ih); +};src/layout/ImageUpload/ImageCropper.tsx (3)
22-30
: Thread readOnly through the cropper.Expose readOnly so initial upload UI respects form state.
interface ImageCropperProps { baseComponentId: string; cropArea: CropArea; validFileEndings?: string[]; + readOnly?: boolean; } -export function ImageCropper({ baseComponentId, cropArea, validFileEndings }: ImageCropperProps) { +export function ImageCropper({ baseComponentId, cropArea, validFileEndings, readOnly }: ImageCropperProps) {
171-177
: Use the readOnly prop (don’t hardcode false).<ImageDropzone baseComponentId={baseComponentId} onDrop={(files) => handleFileUpload(files[0])} acceptedFiles={acceptedFiles} - readOnly={false} + readOnly={!!readOnly} hasErrors={!!validationErrors && validationErrors?.length > 0} />
183-218
: containerRef won’t attach — AppCard isn’t a forwardRef.Wrap in a DOM element or convert AppCard to forwardRef.
- return ( - <AppCard - variant='default' - mediaPosition='top' - ref={containerRef} - media={ + return ( + <div ref={containerRef}> + <AppCard + variant='default' + mediaPosition='top' + media={ <ImageCanvas canvasRef={canvasRef} containerRef={containerRef} imageRef={imageRef} zoom={zoom} position={position} cropArea={cropArea} baseComponentId={baseComponentId} onPositionChange={handlePositionChange} onZoomChange={handleZoomChange} /> - } - > + } + > @@ - <ValidationMessages validationErrors={validationErrors} /> - </AppCard> + <ValidationMessages validationErrors={validationErrors} /> + </AppCard> + </div>src/app-components/Dropzone/Dropzone.tsx (1)
50-63
: LGTM: accept mapping and drag-active callback.The new acceptedFiles passthrough and onDragActiveChange effect are correct and align with react-dropzone’s API.
src/layout/ImageUpload/ImageControllers.tsx (2)
114-121
: Verify accept string correctness.accept relies on acceptedFiles.input. Ensure it includes dot prefixes (e.g., “.jpg,.png”) to work across browsers. See suggested refactor in imageUploadUtils.ts.
94-103
: Icon-only Reset button needs an accessible name.Add aria-label on the Button; icon title isn’t a reliable accessible name.
- <Button - onClick={onReset} - variant='tertiary' - icon={true} - > - <ArrowUndoIcon - title={langAsString('image_upload_component.reset')} - className={classes.resetButton} - /> - </Button> + <Button + onClick={onReset} + variant='tertiary' + icon + aria-label={langAsString('image_upload_component.reset')} + > + <ArrowUndoIcon className={classes.resetButton} /> + </Button>
export const getAcceptedFiles = (validFileEndings?: string[]): AcceptedFiles => { | ||
if (!validFileEndings?.length) { | ||
return { | ||
dropzone: { 'image/*': [] }, | ||
input: 'image/*', | ||
}; | ||
} | ||
|
||
return { | ||
dropzone: mapExtensionToAcceptMime(validFileEndings), | ||
input: validFileEndings.join(','), | ||
}; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accept string for file input may be invalid without dot prefixes.
When validFileEndings are bare (e.g., 'jpg'), browsers won’t filter unless prefixed with '.'.
export const getAcceptedFiles = (validFileEndings?: string[]): AcceptedFiles => {
if (!validFileEndings?.length) {
return {
dropzone: { 'image/*': [] },
input: 'image/*',
};
}
return {
dropzone: mapExtensionToAcceptMime(validFileEndings),
- input: validFileEndings.join(','),
+ input: validFileEndings
+ .map((e) => e.trim())
+ .filter(Boolean)
+ .map((e) => (e.startsWith('.') ? e : `.${e}`))
+ .join(','),
};
};
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
export const getAcceptedFiles = (validFileEndings?: string[]): AcceptedFiles => { | |
if (!validFileEndings?.length) { | |
return { | |
dropzone: { 'image/*': [] }, | |
input: 'image/*', | |
}; | |
} | |
return { | |
dropzone: mapExtensionToAcceptMime(validFileEndings), | |
input: validFileEndings.join(','), | |
}; | |
}; | |
export const getAcceptedFiles = (validFileEndings?: string[]): AcceptedFiles => { | |
if (!validFileEndings?.length) { | |
return { | |
dropzone: { 'image/*': [] }, | |
input: 'image/*', | |
}; | |
} | |
return { | |
dropzone: mapExtensionToAcceptMime(validFileEndings), | |
input: validFileEndings | |
.map((e) => e.trim()) | |
.filter(Boolean) | |
.map((e) => (e.startsWith('.') ? e : `.${e}`)) | |
.join(','), | |
}; | |
}; |
🤖 Prompt for AI Agents
In src/layout/ImageUpload/imageUploadUtils.ts around lines 159 to 171, the
accept string produced for the native file input can be invalid when
validFileEndings are bare like "jpg" because browsers require dot-prefixed
extensions (".jpg"); update the function to normalize validFileEndings before
joining by mapping each entry to a proper accept token: if the entry already
contains a '/' (looks like a mime type) leave it as-is, if it starts with '.'
leave it as-is, otherwise prefix with '.'; use the normalized array to build the
input accept string (join with ',') and ensure mapExtensionToAcceptMime is fed
the normalized extensions for the dropzone path so both dropzone and input
accept values are correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (3)
src/layout/ImageUpload/ImageCanvas.tsx (1)
115-128
: Consider accessibility improvement for keyboard navigationThe keyboard navigation implementation is functional but could benefit from visual feedback. Consider adding a focus indicator or announcing the position changes to screen readers.
Consider adding an announcement for screen readers when the position changes:
const handleKeyDown = (e: React.KeyboardEvent<HTMLCanvasElement>) => { const moveAmount = 10; const keyMap: Record<string, () => void> = { - ArrowUp: () => onPositionChange({ ...position, y: position.y - moveAmount }), - ArrowDown: () => onPositionChange({ ...position, y: position.y + moveAmount }), - ArrowLeft: () => onPositionChange({ ...position, x: position.x - moveAmount }), - ArrowRight: () => onPositionChange({ ...position, x: position.x + moveAmount }), + ArrowUp: () => { + onPositionChange({ ...position, y: position.y - moveAmount }); + // Consider announcing position change for screen readers + }, + ArrowDown: () => { + onPositionChange({ ...position, y: position.y + moveAmount }); + }, + ArrowLeft: () => { + onPositionChange({ ...position, x: position.x - moveAmount }); + }, + ArrowRight: () => { + onPositionChange({ ...position, x: position.x + moveAmount }); + }, };src/layout/ImageUpload/ImageCropper.tsx (1)
160-165
: Type definition placement is unconventionalThe
UpdateImageState
type is defined inside the component function. Move it outside for better code organization.+type UpdateImageState = { minZoom?: number; img?: HTMLImageElement | null }; + export function ImageCropper({ baseComponentId, cropArea, validFileEndings }: ImageCropperProps) { // ... component code ... - type UpdateImageState = { minZoom?: number; img?: HTMLImageElement | null }; const updateImageState = ({ minZoom = minAllowedZoom, img = imageRef.current }: UpdateImageState) => {src/layout/ImageUpload/ImageCanvas.module.css (1)
15-15
: Use design token for neutral background
Replace the hardcoded#f4f5f6
with the CSS variablevar(--ds-color-neutral-3)
from @digdir/designsystemet for consistency.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/layout/ImageUpload/ImageCanvas.module.css
(1 hunks)src/layout/ImageUpload/ImageCanvas.tsx
(1 hunks)src/layout/ImageUpload/ImageCropper.tsx
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/layout/ImageUpload/ImageCanvas.tsx
src/layout/ImageUpload/ImageCropper.tsx
**/*.module.css
📄 CodeRabbit inference engine (CLAUDE.md)
Use CSS Modules for component styling and follow existing patterns in
*.module.css
files
Files:
src/layout/ImageUpload/ImageCanvas.module.css
🧬 Code graph analysis (2)
src/layout/ImageUpload/ImageCanvas.tsx (3)
src/layout/ImageUpload/imageUploadUtils.ts (5)
Position
(3-3)CropArea
(10-10)imagePlacement
(56-63)cropAreaPlacement
(68-72)drawCropArea
(81-89)src/layout/ImageUpload/useImageFile.tsx (1)
useImageFile
(17-52)src/features/language/useLanguage.ts (1)
useLanguage
(90-93)
src/layout/ImageUpload/ImageCropper.tsx (6)
src/layout/ImageUpload/imageUploadUtils.ts (10)
CropArea
(10-10)getAcceptedFiles
(159-171)Position
(3-3)ValidationErrors
(124-124)calculateMinZoom
(120-121)constrainToArea
(36-47)imagePlacement
(56-63)validateFile
(125-152)cropAreaPlacement
(68-72)drawCropArea
(81-89)src/layout/ImageUpload/useImageFile.tsx (1)
useImageFile
(17-52)src/layout/ImageUpload/ImageDropzone.tsx (1)
ImageDropzone
(21-63)src/layout/ImageUpload/ImageCanvas.tsx (1)
ImageCanvas
(25-161)src/layout/ImageUpload/ImageControllers.tsx (1)
ImageControllers
(26-152)src/features/language/Lang.tsx (1)
Lang
(15-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Analyze (javascript)
- GitHub Check: Install
- GitHub Check: Type-checks, eslint, unit tests and SonarCloud
🔇 Additional comments (4)
src/layout/ImageUpload/ImageCanvas.tsx (2)
69-72
: Redraw on image load; otherwise canvas may never render the imageThe draw() function only runs on mount/prop updates. If the image isn't complete at first render, no redraw occurs when it finishes loading.
82-95
: Pointer move lacks pointerId filter and cleanup; also unconstrained panning
- Multiple pointers can interfere because move events aren't filtered by the initial pointerId
- Listeners aren't cleaned up if the component unmounts mid-drag
- Panning isn't constrained to keep the image covering the crop area
src/layout/ImageUpload/ImageCropper.tsx (2)
22-26
: Thread readOnly through to the DropzoneAdd readOnly to props so the initial upload UI respects form state.
183-198
: AppCard is not a forwardRef componentThe
AppCard
component is a plain function component and won't accept refs. You'll need to either wrap it in a div or update AppCard to useReact.forwardRef
.
/publish |
PR release:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (12)
src/layout/ImageUpload/imageUploadUtils.ts (4)
34-45
: Use naturalWidth/naturalHeight to avoid CSS pixel distortion.
image.width/height
reflect CSS layout dimensions, not intrinsic pixels. For canvas math, usenaturalWidth/naturalHeight
and guard against zero to prevent NaN/Infinity in clamp calculations.Apply this diff:
export function constrainToArea({ image, zoom, position, cropArea }: ConstrainToAreaParams): Position { - const scaledWidth = image.width * zoom; - const scaledHeight = image.height * zoom; + const iw = image.naturalWidth || image.width || 1; + const ih = image.naturalHeight || image.height || 1; + const scaledWidth = iw * zoom; + const scaledHeight = ih * zoom; const clampX = scaledWidth > cropArea.width ? (scaledWidth - cropArea.width) / 2 : 0; const clampY = scaledHeight > cropArea.height ? (scaledHeight - cropArea.height) / 2 : 0; const newX = Math.max(-clampX, Math.min(position.x, clampX)); const newY = Math.max(-clampY, Math.min(position.y, clampY)); return { x: newX, y: newY }; }
54-61
: Use naturalWidth/naturalHeight for accurate image scaling.Same issue as constrainToArea: CSS dimensions can differ from intrinsic size.
Apply this diff:
export const imagePlacement = ({ canvas, img, zoom, position }: ImagePlacementParams) => { - const scaledWidth = img.width * zoom; - const scaledHeight = img.height * zoom; + const iw = img.naturalWidth || img.width || 1; + const ih = img.naturalHeight || img.height || 1; + const scaledWidth = iw * zoom; + const scaledHeight = ih * zoom; const imgX = (canvas.width - scaledWidth) / 2 + position.x; const imgY = (canvas.height - scaledHeight) / 2 + position.y; return { imgX, imgY, scaledWidth, scaledHeight }; };
98-115
: Guard zoom math against non-positive values and clamp results.
Math.log
with non-positive inputs yields-Infinity
orNaN
. Also clamplogToNormalZoom
output to[0, 100]
to keep slider domain stable.Apply this diff:
function getLogValues({ minZoom, maxZoom }: ZoomParams): { logScale: number; logMin: number } { + if (minZoom <= 0 || maxZoom <= 0 || minZoom > maxZoom) { + throw new Error('minZoom and maxZoom must be positive and minZoom <= maxZoom'); + } const logMin = Math.log(minZoom); const logMax = Math.log(maxZoom); return { logScale: (logMax - logMin) / 100, logMin }; } export function normalToLogZoom({ value, minZoom, maxZoom }: CalculateZoomParams): number { const { logScale, logMin } = getLogValues({ minZoom, maxZoom }); - return Math.exp(logMin + logScale * value); + const result = Math.exp(logMin + logScale * value); + return Math.max(minZoom, Math.min(maxZoom, result)); } export function logToNormalZoom({ value, minZoom, maxZoom }: CalculateZoomParams): number { const { logScale, logMin } = getLogValues({ minZoom, maxZoom }); if (logScale === 0) { return 0; } - return (Math.log(value) - logMin) / logScale; + const safeValue = Math.max(minZoom, Math.min(maxZoom, value)); + const normalized = (Math.log(safeValue) - logMin) / logScale; + return Math.max(0, Math.min(100, normalized)); }
118-119
: Use naturalWidth/naturalHeight for min-zoom calculation.Consistent with other image dimension uses.
Apply this diff:
export const calculateMinZoom = ({ img, cropArea }: CalculateMinZoomParams) => - Math.max(cropArea.width / img.width, cropArea.height / img.height); + { + const iw = img.naturalWidth || img.width || 1; + const ih = img.naturalHeight || img.height || 1; + return Math.max(cropArea.width / iw, cropArea.height / ih); + };src/layout/ImageUpload/ImageDropzone.tsx (1)
18-36
: ARIA structure needs correction: move dragLabelId to labelId prop.The
dragLabelId
should be passed aslabelId
(foraria-labelledby
), not included indescribedBy
(aria-describedby
). This ensures screen readers correctly announce the dropzone label.Apply this diff:
- const ariaDescribedBy = [descriptionId, dragLabelId, formatLabelId].filter(Boolean).join(' '); + const ariaDescribedBy = [descriptionId, formatLabelId].filter(Boolean).join(' ') || undefined; return ( <Dropzone id={baseComponentId} readOnly={readOnly} onDrop={onDrop} onDragActiveChange={setDragActive} hasValidationMessages={hasErrors} acceptedFiles={{ 'image/*': [] }} + labelId={dragLabelId} data-color='neutral' className={cn(classes.placeholder, { [classes.dragActive]: dragActive })} describedBy={ariaDescribedBy} >src/layout/ImageUpload/ImageCropper.tsx (4)
21-36
: Add readOnly prop to control upload state.The component should accept a
readOnly
prop to disable uploads when the form is read-only.Apply this diff:
interface ImageCropperProps { baseComponentId: string; cropArea: CropArea; + readOnly?: boolean; } const MAX_ZOOM = 5; -export function ImageCropper({ baseComponentId, cropArea }: ImageCropperProps) { +export function ImageCropper({ baseComponentId, cropArea, readOnly = false }: ImageCropperProps) {
94-115
: Memory leak: Image and FileReader not cleaned up on unmount or re-upload.If the component unmounts while loading or a new file is uploaded before the previous one finishes, the old Image/FileReader continues to execute stale callbacks.
Apply this diff:
+ const loadingImageRef = useRef<HTMLImageElement | null>(null); + const fileReaderRef = useRef<FileReader | null>(null); + const handleFileUpload = (file: File) => { const validationErrors = validateFile(file); setValidationErrors(validationErrors); if (validationErrors.length > 0) { return; } + // Clean up any previous loading operations + if (loadingImageRef.current) { + loadingImageRef.current.onload = null; + loadingImageRef.current.src = ''; + loadingImageRef.current = null; + } + if (fileReaderRef.current) { + fileReaderRef.current.onload = null; + fileReaderRef.current = null; + } + const reader = new FileReader(); + fileReaderRef.current = reader; reader.onload = (event) => { const result = event.target?.result; if (typeof result === 'string') { const img = new Image(); + loadingImageRef.current = img; img.id = file.name; img.onload = () => { + if (loadingImageRef.current === img) { updateImageState({ minZoom: calculateMinZoom({ img, cropArea }), img }); + loadingImageRef.current = null; + } }; img.src = result; } }; reader.readAsDataURL(file); }; + + // Clean up on unmount + React.useEffect(() => { + return () => { + if (loadingImageRef.current) { + loadingImageRef.current.onload = null; + loadingImageRef.current.src = ''; + } + if (fileReaderRef.current) { + fileReaderRef.current.onload = null; + } + }; + }, []);
117-146
: Add error handling for toBlob failure.
toBlob
can fail silently. Log errors and provide user feedback when blob creation fails.Apply this diff:
cropCanvas.toBlob((blob) => { if (!blob) { + console.error('Failed to create image blob from canvas'); + setValidationErrors(['image_upload_component.error_save_failed']); return; } const fileName = img?.id || 'cropped-image.png'; const imageFile = new File([blob], fileName, { type: 'image/png' }); saveImage(imageFile); setValidationErrors(null); }, 'image/png');
165-177
: Pass readOnly prop to ImageDropzone.Once
readOnly
is added to props, thread it through here.Apply this diff (after adding readOnly to props):
<ImageDropzone baseComponentId={baseComponentId} onDrop={(files) => handleFileUpload(files[0])} - readOnly={false} + readOnly={readOnly} hasErrors={!!validationErrors && validationErrors?.length > 0} />src/layout/ImageUpload/ImageControllers.tsx (3)
41-48
: Fix zoom normalization: divide slider value by 100.The slider range is 0-100, but
normalToLogZoom
expects 0-1. The incoming value must be divided by 100.Apply this diff:
const handleSliderZoom = (e: React.ChangeEvent<HTMLInputElement>) => { const logarithmicZoomValue = normalToLogZoom({ - value: parseFloat(e.target.value), + value: parseFloat(e.target.value) / 100, minZoom, maxZoom, }); updateZoom(logarithmicZoomValue); };
81-90
: Fix zoom normalization: multiply slider value by 100.
logToNormalZoom
returns 0-1, but the slider expects 0-100. Multiply the result by 100.Apply this diff:
<input id={zoomId} type='range' min='0' max='100' step='0.5' - value={logToNormalZoom({ value: zoom, minZoom, maxZoom })} + value={logToNormalZoom({ value: zoom, minZoom, maxZoom }) * 100} onChange={handleSliderZoom} className={classes.zoomSlider} />
91-100
: Add aria-label for icon-only Reset button.The
title
attribute is a tooltip but not read by screen readers in button context. Addaria-label
for accessibility.Apply this diff:
<Button onClick={onReset} variant='tertiary' icon={true} + aria-label={langAsString('image_upload_component.reset')} > - <ArrowUndoIcon - title={langAsString('image_upload_component.reset')} - className={classes.resetButton} - /> + <ArrowUndoIcon className={classes.resetButton} /> </Button>
🧹 Nitpick comments (1)
src/layout/ImageUpload/ImageDropzone.tsx (1)
45-48
: Make "find" text a clickable label for better UX.Wrapping the "find" text in a
<label htmlFor={baseComponentId}>
would make it clickable and open the file picker, improving user experience.Apply this diff:
<> <Lang id='form_filler.file_uploader_drag' /> - <span className={classes.blueUnderLine}> + <label + htmlFor={baseComponentId} + className={classes.blueUnderLine} + > {' '} <Lang id='form_filler.file_uploader_find' /> - </span> + </label> </>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
src/language/texts/en.ts
(1 hunks)src/language/texts/nb.ts
(1 hunks)src/language/texts/nn.ts
(1 hunks)src/layout/ImageUpload/ImageControllers.tsx
(1 hunks)src/layout/ImageUpload/ImageCropper.tsx
(1 hunks)src/layout/ImageUpload/ImageDropzone.tsx
(1 hunks)src/layout/ImageUpload/ImageUploadComponent.tsx
(1 hunks)src/layout/ImageUpload/config.ts
(1 hunks)src/layout/ImageUpload/imageUploadUtils.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- src/language/texts/nb.ts
- src/language/texts/en.ts
- src/layout/ImageUpload/ImageUploadComponent.tsx
- src/language/texts/nn.ts
- src/layout/ImageUpload/config.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}
: Avoid usingany
and unnecessary type casts (as Type
) in TypeScript; prefer precise typings and refactor existing casts/anys
For TanStack Query, use objects to manage query keys and functions, and centralize shared options viaqueryOptions
Files:
src/layout/ImageUpload/ImageDropzone.tsx
src/layout/ImageUpload/ImageCropper.tsx
src/layout/ImageUpload/imageUploadUtils.ts
src/layout/ImageUpload/ImageControllers.tsx
🧬 Code graph analysis (3)
src/layout/ImageUpload/ImageDropzone.tsx (4)
src/app-components/Dropzone/Dropzone.tsx (2)
IDropzoneProps
(15-27)Dropzone
(31-99)src/hooks/useDeviceWidths.ts (1)
useIsMobileOrTablet
(33-35)src/components/label/Label.tsx (1)
getDescriptionId
(135-141)src/features/language/Lang.tsx (1)
Lang
(15-23)
src/layout/ImageUpload/ImageCropper.tsx (6)
src/layout/ImageUpload/imageUploadUtils.ts (8)
CropArea
(8-8)Position
(1-1)calculateMinZoom
(118-119)constrainToArea
(34-45)imagePlacement
(54-61)validateFile
(121-141)cropAreaPlacement
(66-70)drawCropArea
(79-87)src/layout/ImageUpload/useImageFile.tsx (1)
useImageFile
(17-52)src/layout/ImageUpload/ImageDropzone.tsx (1)
ImageDropzone
(18-58)src/layout/ImageUpload/ImageCanvas.tsx (1)
ImageCanvas
(25-161)src/layout/ImageUpload/ImageControllers.tsx (1)
ImageControllers
(24-149)src/features/language/Lang.tsx (1)
Lang
(15-23)
src/layout/ImageUpload/ImageControllers.tsx (4)
src/features/attachments/index.ts (1)
UploadedAttachment
(20-20)src/features/language/useLanguage.ts (1)
useLanguage
(90-93)src/layout/ImageUpload/imageUploadUtils.ts (2)
normalToLogZoom
(104-107)logToNormalZoom
(109-115)src/features/language/Lang.tsx (1)
Lang
(15-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Type-checks, eslint, unit tests and SonarCloud
- GitHub Check: Install
🔇 Additional comments (15)
src/layout/ImageUpload/imageUploadUtils.ts (5)
1-8
: LGTM: Clean type definitions.The Position, CropForm, CropAreaParams, and CropArea types are well-defined and appropriately scoped.
10-25
: LGTM: Sensible default crop area logic.The function correctly normalizes Circle crops to equal dimensions and provides reasonable defaults.
66-70
: LGTM: Correct crop area centering.The function properly centers the crop area within the canvas.
79-87
: LGTM: Proper path drawing for Circle and Square crops.The drawCropArea function correctly uses arc for Circle and rect for Square.
121-141
: LGTM: File validation logic is sound.The function correctly checks for image MIME type and enforces a 10MB size limit.
src/layout/ImageUpload/ImageDropzone.tsx (1)
1-16
: LGTM: Clean imports and type definition.Props are well-typed and appropriate for the component's needs.
src/layout/ImageUpload/ImageCropper.tsx (5)
40-55
: LGTM: Position constraint logic is correct.The handler properly constrains position changes using the
constrainToArea
utility.
58-92
: LGTM: Zoom logic maintains viewport center.The zoom handler correctly preserves the image point under the viewport center during zoom changes.
148-163
: LGTM: Delete and cancel handlers are correct.The handlers properly clean up state and call the appropriate hooks.
179-211
: LGTM: Cropping UI rendering is well-structured.The card layout with ImageCanvas and ImageControllers is appropriately composed.
214-227
: LGTM: ValidationMessages component is simple and correct.Based on maintainer feedback,
Lang
handles undefined ids gracefully.src/layout/ImageUpload/ImageControllers.tsx (4)
1-22
: LGTM: Clean imports and well-typed props.The component interface is clear and appropriately structured.
35-57
: LGTM: Unique IDs and file input handling are correct.The use of
useId
ensures unique IDs across instances, and the file input is properly cleared after selection.
59-72
: LGTM: Delete button correctly reflects storedImage state.The button is appropriately disabled during upload or deletion.
103-148
: LGTM: Action buttons are well-structured and accessible.The Save, Change, and Cancel buttons have appropriate labels and keyboard handling.
|
Description
Related Issue(s)
Verification/QA
kind/*
andbackport*
label to this PR for proper release notes groupingSummary by CodeRabbit
New Features
Style
API
Tests