Skip to content

feat(image-editor): add Angular image editor#36236

Open
oidacra wants to merge 22 commits into
mainfrom
issue-36063-image-editor-build-dotimageeditorcomponent-modal-s
Open

feat(image-editor): add Angular image editor#36236
oidacra wants to merge 22 commits into
mainfrom
issue-36063-image-editor-build-dotimageeditorcomponent-modal-s

Conversation

@oidacra

@oidacra oidacra commented Jun 18, 2026

Copy link
Copy Markdown
Member

Summary

CleanShot.2026-06-24.at.16.03.56.mp4

Replaces the legacy Dojo ImageEditor.js with a new standalone Angular library
@dotcms/image-editor, wired into Edit Content's binary field through an
IMAGE_EDITOR_LAUNCHER seam (Angular-only path).

The editor is a viewer of a dotCMS endpoint: the <img> src is a computed
/contentAsset/image/{id}/{field}/filter/... URL, so every control change rebuilds
the URL and the server renders the adjusted image — no client-side pixel work.

State is an events-based NgRx Signal Store (@ngrx/signals/events), composed of
one vertical signalStoreFeature per area of functionality (adjust, transform,
crop, focal point, file info, tools, history, asset, preview, download) — each
bundling its own reducers, selectors and effects.

Scope note

This PR delivers the editor shell, the server-side preview pipeline, the panels and
the crop/focal tools, plus Download. Saving the edited image back to the field
is intentionally deferred to #36067
— the editor currently previews and downloads.

What's included

  • Library @dotcms/image-editor: root dialog, header, canvas (crossfade, zoom +
    pan, crop/focal overlays), side panels (Adjust / Transform / File info / History),
    footer (Cancel + Download).
  • Server-side preview via the filter-URL builder (single source of truth) and Download.
  • Command history: removable applied-edits list + undo/redo, keyboard shortcuts
    (Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z, Ctrl+Y), ignored while a text field is focused.
  • Launcher seam in libs/edit-content (token + Angular launcher) and binary-field wiring.
  • Canvas interactions: zoom with drag-to-pan, fit-to-screen, crop-to-current-view,
    focal-centered aspect crop.

Preview robustness

  • Preview fetched as a complete, verified blob (local object URL), so a
    partially-generated server response can't paint a truncated frame; incomplete
    responses are detected and retried silently.
  • decode() + natural-dimension gate before promoting a frame.

Code organization

  • Constants in one image-editor.constants.ts; types/models in models/image-editor.models.ts.
  • Store split into store/features/with-*.feature.ts + store-utils.ts; store.ts is a thin composition.

Testing

  • nx lint image-editor / nx lint edit-content — clean
  • nx test image-editor258 passing (per-feature unit specs + store-utils + dimensions.util + integration; store-feature branch coverage ~100%)
  • nx test edit-content1961 passing (28 skipped)
  • Verified live (local dotCMS, demo content): preview, crop, focal, zoom/pan, fit, download, undo/redo (incl. Cmd+Z), panel persistence.

Issues

@claude

claude Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @oidacra's task in 3m 14s —— View job


Rollback Safety Analysis

  • Read rollback-unsafe categories reference
  • Get full PR diff
  • Analyze diff against all unsafe categories
  • Apply label

Result: ✅ Safe to Rollback

All changes in this PR were checked against every category in the rollback-unsafe reference:

Category Verdict
C-1 Structural data model change ✅ No DB schema changes at all
C-2 Elasticsearch mapping change ✅ No ES mapping touched
C-3 Content JSON model version bump ✅ No CURRENT_MODEL_VERSION change
C-4 DROP TABLE / DROP COLUMN ✅ No DDL drops
H-1 One-way data migration ✅ No runonce task, no data transform
H-2 RENAME TABLE / RENAME COLUMN ✅ No renames
H-3 PK restructuring ✅ No PK changes
H-4 New ContentType field type ✅ No new Field subclass
H-5 Binary storage provider change ✅ No StoragePersistenceProvider change
H-6 DROP PROCEDURE / DROP FUNCTION ✅ No stored procedure changes
H-7 NOT NULL column without default ✅ No new DB columns
H-8 VTL viewtool contract change ✅ No viewtool or returned-object changes
M-1 Column type change ✅ No column type changes
M-2 Push publishing bundle format ✅ No bundler/handler changes
M-3 REST/GraphQL API contract ✅ Only additive (new feature flag added to allowlist, not a rename/removal)
M-4 OSGi plugin API breakage ✅ No OSGi interface changes

Summary of Java changes:

  • FeatureFlagName.java — new constant FEATURE_FLAG_NEW_IMAGE_EDITOR added (additive)
  • ConfigurationResource.java — new flag appended to two existing sets (additive)
  • dotmarketing-config.properties — new property FEATURE_FLAG_NEW_IMAGE_EDITOR=false (off by default)
  • Language.properties — new i18n keys for image editor UI (additive)

All backend changes are purely additive and gated behind a feature flag that defaults to false. Rolling back to N-1 leaves the flag unrecognized (it simply isn't in the allowlist), which is harmless — N-1 ignores unknown flags. No data migration, no schema change, no contract breakage.

The label AI: Safe To Rollback has been applied.

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

🤖 Bedrock Review — deepseek.v3.2

[🟡 Medium] core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component.ts:125#imageEditorLauncher is injected with { optional: true } but onEditImage() doesn't handle the case where it's null (when isAvailable() returns false). The method will call launcher.open() on null, causing a runtime error. This matters because the feature flag could be off, or the launcher might not be provided in non-Angular contexts.

[🟡 Medium] core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component.ts:395 — The onEditImage method calls launcher.open() without checking if launcher is defined after the !launcher?.isAvailable() guard. If launcher is null but isAvailable() would be false, the guard passes (because !null?.isAvailable() is true), then the method proceeds to the legacy path. This is correct, but the comment on line 125 incorrectly states it "safely no-ops". It should clarify that the fallback works.

[🟡 Medium] core-web/libs/edit-content/src/lib/fields/shared/image-editor-launcher/angular-image-editor.launcher.ts:34#enabled signal uses initialValue: false. This means isAvailable() returns false until the server responds, causing a brief flash of the legacy editor even if the feature flag is enabled. This could cause a race condition where the user clicks "edit" before the flag loads, triggering the legacy editor incorrectly. Consider if this is acceptable or if the UI should be disabled until the flag is known.

[🟡 Medium] core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.ts:452 — The #cropOverlay view child is typed as DotImageEditorCropOverlayComponent but the template uses dot-image-editor-crop-overlay without a template reference variable (#). The view child will not find the component instance, causing applyCrop() and cancelCrop() calls in the footer to fail. This matters because the crop overlay's apply/cancel actions will be broken.

[🟡 Medium] core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.ts:454 — The #focalOverlay view child has the same issue: no template reference variable, so it will be undefined. The focal overlay interactions may fail.

[🟡 Medium] core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.ts:456 — The #displayImg view child is used to observe the image's rendered rect via ResizeObserver. However, the displayImg element is referenced in the template only by #displayImg on the <img> tag, which is correct. Ensure the ResizeObserver is properly cleaned up in DestroyRef to prevent memory leaks.

[🟠 High] core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.spec.ts:40 — The test suite globally mocks URL.createObjectURL and URL.revokeObjectURL. This is a shared resource; other tests in the same test run may rely on the real implementation. This could cause flaky tests or false positives. It's safer to mock per-test or restore after each test.

[🟡 Medium] core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.spec.ts:45 — The test stubs HTMLImageElement.prototype.decode globally. This affects all tests running in the same context, potentially breaking other specs. Use jest.spyOn(instance, 'decode') instead.

[🟡 Medium] core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.spec.ts:48naturalWidth and naturalHeight are defined as getters on the prototype, affecting all image elements. This could cause unrealistic test behavior elsewhere. Prefer mocking per instance.

[🟡 Medium] core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.spec.ts:344 — The dispatchedEvent helper function searches for events by type suffix, which is fragile if event types change. It's used across multiple tests; a change in event naming could break all tests. Consider a more robust method.

[🟡 Medium] core-web/libs/edit-content/src/lib/edit-content.shell.component.ts:25DialogService is provided at the component level. This is correct for scoping, but ensure that any parent injector doesn't already provide DialogService (which could cause conflicts). The comment explains the intent, which is good.

[🟡 Medium] core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component.spec.ts:359 — The test 'should fall back to the legacy Dojo editor when the new editor is disabled' mocks DotBinaryFieldEditImageService.prototype.openImageEditor. This modifies the prototype, which could affect other tests running in the same suite. Use jest.spyOn(instance, 'openImageEditor') instead.

[🟡 Medium] core-web/libs/edit-content/src/lib/fields/shared/image-editor-launcher/angular-image-editor.launcher.spec.ts:15featureFlag$ is a BehaviorSubject but the test uses featureFlag$.next(false) to simulate the flag being off. However, the service's #enabled signal is derived from getFeatureFlagWithDefault, which returns the subject. Changing the subject after service creation tests the reactive behavior, which is correct. Ensure the service is recreated for each test to avoid state leakage.

The PR introduces a new image editor with a feature flag and fallback to legacy. The changes are mostly clean, but there are a few issues with optional injection handling, view child references, and test pollution that need attention.


Run: #28042028043 · tokens: in: 21199 · out: 1407 · total: 22606

@oidacra oidacra force-pushed the issue-36063-image-editor-build-dotimageeditorcomponent-modal-s branch from 03448ba to c2f4635 Compare June 18, 2026 21:27
@oidacra oidacra force-pushed the issue-36063-image-editor-build-dotimageeditorcomponent-modal-s branch from c2f4635 to a1d5b80 Compare June 19, 2026 14:46
@oidacra oidacra force-pushed the issue-36063-image-editor-build-dotimageeditorcomponent-modal-s branch from a1d5b80 to 5f38ef3 Compare June 19, 2026 14:51
@oidacra oidacra changed the title feat(image-editor): add Angular image editor with events-based signalStore feat(image-editor): add Angular image editor Jun 19, 2026
@oidacra oidacra marked this pull request as ready for review June 22, 2026 17:14
@oidacra oidacra requested review from hmoreras and nicobytes June 22, 2026 17:15
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

🤖 Bedrock Review — qwen.qwen3-next-80b-a3b

New Issues

  • 🟠 High: core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component.ts:409fileName: this.contentlet?.fileName ?? metadata?.name may pass null to ImageEditorOpenParams.fileName, which is non-optional per @dotcms/image-editor type; this causes a runtime type error when the new editor is enabled and both fileName and metadata.name are null.
  • 🟡 Medium: core-web/libs/image-editor/src/lib/components/dot-image-editor-address-bar/dot-image-editor-address-bar.component.ts:103navigator.clipboard.writeText() called without user gesture or focus check — may fail silently in Safari and other browsers; no fallback or user feedback beyond toast on failure.
  • 🟡 Medium: core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.ts:147stageTransform() uses zoomLevel() and panOffset() without bounds checking — may cause CSS transform overflow or layout thrashing if values are extreme or NaN.

Existing

  • 🟠 High: core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component.ts:409 — fileName may be null when both contentlet.fileName and metadata.name are null, but ImageEditorOpenParams.fileName is non-optional
  • 🟡 Medium: core-web/libs/image-editor/src/lib/components/dot-image-editor-address-bar/dot-image-editor-address-bar.component.ts:103 — navigator.clipboard.writeText() called without user gesture or focus check — may fail silently in Safari and other browsers
  • 🟡 Medium: core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.ts:147 — stageTransform() uses zoomLevel() and panOffset() without bounds checking — may cause CSS transform overflow or layout thrashing

Resolved

  • core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component.spec.ts:304 — Legacy Dojo service test removed; replaced with launcher seam test

Run: #28125103711 · tokens: in: 21315 · out: 676 · total: 21991

oidacra added 22 commits June 24, 2026 16:46
…Store (#36063)

New @dotcms/image-editor library — a full-screen "Edit image" modal that renders a
live, server-side preview by building dotCMS /contentAsset/image filter URLs (a viewer
of the endpoint). State is an @ngrx/signals events-based store (eventGroup/withReducer/
on/injectDispatch + rxMethod effects) with adjust/transform/crop/focalPoint/fileInfo/
zoom slices and a coalesced command history (undo/redo + removable applied edits).

- DotImageEditorComponent (OnPush) opened via PrimeNG DialogService
- Canvas with two-layer image crossfade + skeleton/spinner/error+retry
- Tool rail (move/crop/focal), accordion panels (Adjust/Transform/File info/History),
  footer (Cancel/Download/Save split button)
- IMAGE_EDITOR_LAUNCHER seam (Angular/Legacy/Noop); binary field 'Edit image' now opens
  the new editor and saves via the _imageToolSaveFile temp-file flow
- 79 edit.content.image-editor.* i18n keys; Storybook story for isolated testing

Closes #36063
- Accordion: design-aligned header (13px title, 11px subtitle, primary icon
  chip when open), collapsed by default, open sections persisted to localStorage
- Adjust values are editable number fields synced with the sliders (clamped)
- Undo/redo keyboard shortcuts on the dialog (Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z,
  Ctrl+Y); ignored while a text field is focused
- Crop: Shift-drag a corner locks the starting aspect ratio
- Address bar text/icon use the design's 78%-white dark-chrome tone
- Canvas/footer layout + padding, gradient adjust sliders, taller dialog
- Store: split panel events into Adjust/Transform/File-info groups, one
  withReducer per group, and move async effects to withEventHandlers
- Remove unused Storybook wiring (story + .storybook glob) and the unused
  legacy Dojo and noop launchers

Refs #36063
…y/focal fixes

- Accordion: flatten app-wide via CustomLaraPreset (square corners, opaque
  sticky section headers, animated chevron); smaller control labels/values
- Transform: editable Scale/Rotate number inputs; Scale now resizes
  (scale% x natural size) instead of being a no-op
- Preview: store-owned auto-retry on transient load failure, decode() +
  natural-dimension completeness guard, hidden pending preloader so
  half-painted frames never show
- History: removing an applied edit replays the remaining flow (field-level
  delta replay) instead of leaving stale effects baked in; de-duplicated and
  shrunk applied-edit labels
- Focal point: set live on drag (no Set click), no preview reload; focal-
  centered aspect crop (1:1 / 16:9 / 4:3) presented in the canvas dark bar;
  removed the no-op FocalPoint preview filter (save-time anchor only)
- File info: Original Size row
- Store: split panel events into Adjust/Transform/File-info groups with
  per-group reducers; effects via withEventHandlers
- i18n: remove orphaned history.category.*, transform.aspect, focal.done keys

Refs #36063
…ted frames

The preview <img> pointed straight at /contentAsset/image/... and painted
progressively, so a partially-generated server response (the server renders
filters on the fly; the first request for a fresh URL races that generation)
showed as a truncated band — and the browser still fired `load` because the
file header carrying the dimensions arrived intact, so the decode()+dimensions
guard could not catch it. A manual refresh fixed it by re-requesting the now
cached, complete file.

- Service: add loadPreviewImage(url) — GET the URL as a blob and return a local
  object URL only for a complete image. A stream truncated against Content-Length
  errors the request outright; an explicit length mismatch, an empty body, or an
  HTML/JSON error body (200 while still generating) is rejected.
- Canvas: fetch each queued preview via loadPreviewImage (switchMap cancels a
  superseded in-flight request) and render the pending/displayed layers from the
  verified object URLs, so the <img> can never paint half an image. Manage the
  object-URL lifecycle (promote on decode, revoke the replaced one, clean up on
  destroy).
- Store: raise the silent preview-retry budget to 3 so a generation race resolves
  invisibly (mirroring a manual refresh) before the error UI is shown.

Refs #36063
…eview fixes

Functional fixes:
- Undo/redo shortcut now listens on document:keydown (in a DynamicDialog focus
  usually sits outside the component, so a host-only keydown never fired)
- Pan a zoomed-in image by dragging (grab cursor; move tool); fit and zoom-out
  to <=100% recenter
- Switching to crop while zoomed captures the visible region, resets to fit and
  seeds the crop box to exactly what was framed (crop-to-current-view)

Code-review fixes:
- Drop the duplicate save-failure toast (service rethrows; the store is the single
  surface)
- Zoom-aware overlay coordinates (divide getBoundingClientRect by the stage scale)
  so crop/focal markers don't drift at non-100% zoom
- Remove dead focal-point hydration plumbing (loadAssetMeta never produced it)
- Truncate the redo tail on a same-category edit after an undo
- @HostListener -> host object (overlays + root), per ANGULAR_STANDARDS
- onPendingLoaded re-checks #pending after decode() to avoid promoting a revoked
  object URL under rapid edits
- a11y labels on adjust/quality sliders and the focal aspect group
- private -> # in the adjust/transform panels; ImageRect moved to the models file
- buildFilterChain @param no longer lists a non-existent focalPoint slice

Refactor:
- Extract the store's pure helpers (history coalesce/replay, focal-centered crop,
  context/patch builders, formatters) into image-editor.store-utils.ts; the store
  drops from 817 to 526 lines

Refs #36063
…dentity in service test

Code-review follow-ups (delta review of 9963b75):
- coalesceHistory JSDoc now states the same-category branch also discards the
  redo tail (the summary implied in-place-only; the inline comment was already
  correct)
- saveEditedImage failure test asserts the original HttpErrorResponse propagates
  unchanged (instanceof + status), not just that an error occurred

Refs #36063
…library files

Constants and type declarations were scattered across components, the store and
services. Centralize them so the library has one home for each:

- New image-editor.constants.ts: RANGES, ZOOM_*, crop/focal nudge steps,
  MIN_CROP_SIZE, CROP_HANDLES, BYTES_PER_KB, AUTO_PREVIEW_RETRY_LIMIT,
  COMPRESSION_LABELS, SLICE_KEYS, IMAGE_EDITOR_PANEL_STATE_KEY. The per-panel
  *_MIN/MAX duplicates (ADJUST/SCALE/ROTATE) now reference RANGES (single source).
- models/image-editor.models.ts absorbs every remaining interface/type:
  ImageEditorState, Dimensions, FilterChainInput, ToolRailItem, CompressionOption,
  LocalRect, HandlePosition, NormalizedPoint, SaveEditedImageResponse,
  NaturalDimensions, AssetMeta, EditableSlices, SlicePatch.
- state.ts keeps only the initial-state values; store-utils/store/components import
  the shared constants and types. No behavior change; 196 tests green.

Refs #36063
…y functionality

Split the 524-line store monolith into one signalStoreFeature per area of
functionality (https://ngrx.io/guide/signals/signal-store/custom-store-features),
each bundling its own reducers, selectors and effects so a domain lives in one
file (store/features/with-*.feature.ts):

- withAdjust / withTransform / withCrop / withFocalPoint / withFileInfo / withTools
- withHistory (+ appliedEdits / canUndo / canRedo)
- withAsset (+ loadAsset$ effect)
- withPreview (+ appliedFilters / previewUrl / isDirty / isBusy + resolveSize$)
- withSave (+ canSave + save$ / download$)

Cross-cutting selectors live in their owning feature; withSave declares the
previewUrl / appliedFilters props it consumes from withPreview (so it composes
after it). image-editor.store.ts is now a 41-line composition. Pure
reorganization — the reducers still fold the full flat state; no behavior change,
196 tests green (store spec exercises the public API, unchanged).

Refs #36063
…(branch coverage)

The single integration spec left ~40% of the store features' branches uncovered.
Add focused unit specs that mount a minimal signalStore(withState, withX()) per
feature and exercise every branch in isolation, plus a store-utils spec for the
pure helpers:

- image-editor.store-utils.spec.ts: coalesceHistory (append / in-place / redo-tail
  drop), rebuildHistory (first/middle/last), focalCenteredCrop (wide/tall/centered/
  clamped), contextFromParams, the slice-patch helpers, errorMessage
- features/with-*.feature.spec.ts: adjust (hue/grayscale on+off), transform
  (outputDims branches, scale===100 keep-crop, outputDimensions selector), crop,
  focal-point (no-natural-dims early return, no-reload anchor), file-info (quality),
  tools, history (not-found / redo-tail removal / undo-redo bounds / reset /
  appliedEdits / canUndo / canRedo)
- store.spec.ts: add retryRequested and asset-load-failure cases (effect-level,
  kept in the integration spec)

The general spec stays as the integration layer (composition, save exhaustMap,
debounced size, cross-feature canSave/isBusy). Store features branch coverage
60% -> ~100%; 196 -> 257 tests.

Refs #36063
…put dims)

dimensions.util's pure functions were only exercised indirectly (74% branch).
Add a direct spec covering every branch: clamp bounds, computeResizeParams
(explicit both / width-only / height-only / scale / none) and
computeOutputDimensions (natural, active crop, resize-supersedes-crop, explicit
both, width-only and height-only aspect derivation, zero-height fallback, scale).
100% statements, 97% branch; 257 -> 274 tests.

Refs #36063
…+ download only)

Saving the edited image back to the field is its own issue, so remove the
real-save path from this PR cleanly (no dead scaffolding):

- service: drop saveEditedImage + persistFocalPoint (and #toTempFile)
- store: replace withSave with withDownload (only the download$ effect); drop the
  save events (saveRequested/saveAsRequested/saveSucceeded/saveFailed), the
  saveStatus/savedTempFile state, SaveStatus type and SaveEditedImageResponse;
  isBusy now reflects only previewStatus
- footer: drop the Save / Save-as split button (Cancel + Download remain)
- root dialog: no longer closes with a saved temp file (closes via Cancel/Esc)
- the Angular launcher already maps onClose -> null, so open() now resolves null
- remove the orphaned footer.save/saving/save-as i18n keys
- prune the corresponding tests

The editor is now preview + edit + download; the save issue reintroduces the
save flow as a cohesive unit. image-editor 258 tests green; edit-content
unaffected (1961).

Refs #36063
- Repurpose the canvas maximize control as a full-screen toggle that
  expands the dialog to the viewport and back, easing the resize and
  honouring prefers-reduced-motion; move fit-to-screen onto the
  zoom-value control so it is preserved.
- Resize the host PrimeNG dialog via the injected Dialog instance
  (container()) rather than a DOM query.
- Group the editor's transient view state into one withView feature
  (active tool + isFullscreen), replacing withTools.
- Consolidate full-screen style/transition constants into
  image-editor.constants.ts; add full-screen i18n keys.

Refs #36062
…_IMAGE_EDITOR

The new @dotcms/image-editor opens only when the flag is on; otherwise the
binary field falls back to the legacy Dojo image editor, so behavior is
unchanged by default.

- Frontend: add FEATURE_FLAG_NEW_IMAGE_EDITOR to FeaturedFlags; the Angular
  launcher's isAvailable() resolves the flag via DotPropertiesService;
  onEditImage falls back to the legacy editor when off.
- Backend: declare the flag in FeatureFlagName, expose it in
  ConfigurationResource (boolean set + white list), default it false in
  dotmarketing-config.properties.

Refs #36062
The crop box is drawn in the displayed (flipped/rotated) preview's
coordinates, but buildFilterChain emitted Crop first, so the server cropped
the original image and then flipped it — mirroring the selected region (a
left crop returned the right side under horizontal flip). Emit Crop after
Rotate/Flip so it acts on the image as displayed, matching the legacy editor
which appends Crop to the already-transformed chain.

Refs #36066
… primary, Discard outlined)

Mirror the edit-content unsaved-changes prompt (unsavedChangesGuard) so the
"Discard changes?" confirmation no longer renders two primary buttons. Keep
editing is the primary (accept) action; Discard is the secondary outlined
(reject) action. Dismissals (X / ESC / mask click) now keep editing instead of
discarding. Frontend-only: reuses existing message keys.
The focal overlay converted a painted (zoom-scaled) pointer offset against
imageRect, which is in unscaled (logical) px, so clicks landed off-target at
any zoom other than 100%. Pass the stage zoom scale to the overlay and divide
the painted offset by it in #setFromClient, fixing both click and drag
placement while zoomed in or out.
…oter bands

Switch the editor canvas from dark chrome to a light theme: the image viewer is
white and the address-bar header and footer bands sit one shade darker than white
(surface-100) with muted dark text/icons and hairline dividers. The focal aspect
pills and error overlay are re-toned for the light surface; the floating tool rail
stays dark over the image for contrast.
…next to close

Relocate the full-screen toggle from the canvas address bar to the editor header,
grouped with the close (X) button as the dialog's window controls. The header now
dispatches imageEditorViewEvents; the address bar keeps the URL, zoom and undo/redo
controls. Tests moved accordingly.
…oint, smooth drag

- Remove the Focal Point tool entirely (overlay component, with-focal-point store
  feature, focal events/state/models, focalPointPop animation, nudge constants and
  the tool-rail entry) — out of scope for this editor.
- Move aspect ratios into the crop tool: Free / 1:1 / 16:9 / 4:3 reshape and lock
  the crop box, with editable width/height pixel inputs (disabled on Free).
- Replace the empty gray footer band with a floating action bar revealed on hover.
- Fix crop drag/resize jank: coalesce pointer moves to one update per animation
  frame off the Angular zone, give the size readout value-based equality so a move
  does not re-render the inputs, and replace the viewport-sized box-shadow dim with
  four cheap solid panels (flat 60fps, dim stays visible while dragging).
…th crop/grab tools

- Replace all PrimeIcons and the inline tool-rail SVGs with the official Material
  Symbols font (global; projected into PrimeNG buttons via #icon templates,
  pButton child content, and toggleButton #content).
- Move the move(grab) + crop tools out of the floating tool rail (now deleted) into
  the address bar, restyled as UVE toolbar-center rounded pills: tools | URL | zoom.
  The URL pill grows to fill, pushing the tool and zoom pills to the edges; the
  active tool renders as a white circle inside its pill (the editor's mode).
- White header band; the dialog window-control icons (open_in_full / close) use the
  Material Symbols size (Tailwind ! to beat the global 24px).
…verflows header

- Measure the image via its layout box (offsetLeft/Top/Width/Height) instead of
  getBoundingClientRect()/scale. The stage's transform transition let a mid-flight
  measurement stick ~2% small, so the default crop box fell a few px short of the
  image edges; the layout box is transform-independent and always matches.
- Address-bar pills no longer shrink (flex: 0 0 auto) and the URL pill flexes from
  basis 0, so a long filter-chain URL truncates instead of pushing the zoom and
  undo/redo controls out of the header.
@oidacra oidacra force-pushed the issue-36063-image-editor-build-dotimageeditorcomponent-modal-s branch from 5e89ebc to 43fdfd5 Compare June 24, 2026 20:46
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

🤖 Bedrock Review — qwen.qwen3-next-80b-a3b

New Issues

  • 🔴 Critical: core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component.ts:409fileName: this.contentlet?.fileName ?? metadata?.name may pass null to ImageEditorOpenParams.fileName, which is non-optional in @dotcms/image-editor — causes runtime crash if contentlet is falsy and metadata.name is null
  • 🟠 High: core-web/libs/edit-content/src/lib/fields/shared/image-editor-launcher/angular-image-editor.launcher.ts:58DialogService.open() is called without dismissableMask: false being enforced in the config — allows clicking outside to dismiss, bypassing user intent and potentially losing unsaved edits
  • 🟡 Medium: core-web/libs/image-editor/src/lib/components/dot-image-editor-address-bar/dot-image-editor-address-bar.component.ts:103navigator.clipboard.writeText() called without user gesture or focus check — may fail silently in Safari and other browsers (repeats prior finding)
  • 🟡 Medium: core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.ts:147stageTransform() uses zoomLevel() and panOffset() without bounds checking — may cause CSS transform overflow or layout thrashing (repeats prior finding)

Existing

  • 🟡 Medium: core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component.ts:409 — fileName may be null when both contentlet.fileName and metadata.name are null, but ImageEditorOpenParams.fileName is non-optional (repeats prior finding)
  • 🟡 Medium: core-web/libs/image-editor/src/lib/components/dot-image-editor-address-bar/dot-image-editor-address-bar.component.ts:103 — navigator.clipboard.writeText() called without user gesture or focus check — may fail silently in Safari and other browsers (repeats prior finding)
  • 🟡 Medium: core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.ts:147 — stageTransform() uses zoomLevel() and panOffset() without bounds checking — may cause CSS transform overflow or layout thrashing (repeats prior finding)

Resolved

  • core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component.spec.ts:304 — Legacy Dojo service test removed; replaced with launcher seam test

Run: #28128511180 · tokens: in: 21134 · out: 1001 · total: 22135

@wezell

wezell commented Jun 24, 2026

Copy link
Copy Markdown
Member

Love this so much and am happy to ship as is but here are some of my thoughts:

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

Labels

AI: Safe To Rollback Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

2 participants