feat(image-editor): add Angular image editor#36236
Conversation
|
Claude finished @oidacra's task in 6m 41s —— View job Rollback Safety Analysis
Result: ✅ Safe to Roll BackLabel applied: AI: Safe To Rollback Scope of changes checked:
Categories checked and cleared:
Why rolling back is safe: On N-1, |
🤖 Bedrock Review —
|
03448ba to
c2f4635
Compare
c2f4635 to
a1d5b80
Compare
a1d5b80 to
5f38ef3
Compare
…+ 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.
…n zoomed - Address bar: use --p-surface-* tokens (PrimeNG 21) instead of the undefined --surface-* that fell back to hardcoded gray-200. Pill fill -> surface-100, active tool -> primary-50/700 tint (matches the design's .iem-iconbtn.active), border/divider -> theme slate tokens. - Canvas: constrain the pan offset so a zoomed image always covers the stage; dragging can no longer pull empty space past the top/left (or bottom/right) edge. Re-clamp on zoom in/out.
…pdown
- Add `avif` compression mode -> builds the libvips-only `/filter/avif/avif_q/{q}`
chain (same 0..100 quality as jpeg/webp). Registered lowercase as `avif` in
VipsImageFilterApiImpl; requires IMAGE_API_USE_LIBVIPS=true on the server.
- Replace the compression p-selectButton with a p-select dropdown now that there
are five options (none/auto/jpeg/webp/avif); translated item/selectedItem
templates, appendTo body.
- Wire model (CompressionMode, FilterName), COMPRESSION_LABELS, i18n key, and
builder/panel tests.
…change main dropped DotPropertiesService.getFeatureFlagWithDefault(key, default) in favor of getFeatureFlag(key): Observable<boolean> (the convention the other new-editor flags use). The rebase surfaced this as a build break in the image editor launcher. Switch to getFeatureFlag; the flag still ships as false in dotmarketing-config.properties for rollback safety, and toSignal keeps it off until the server replies.
43fdfd5 to
fe49dea
Compare
🤖 Bedrock Review —
|
…ive zoom %, panels open by default - Crop bar: replace the aspect-preset button row with a dropdown (Free/1:1/16:9/4:3) and add a landscape/portrait orientation toggle. cropAspect is now derived as the preset ratio inverted for portrait; orientation is disabled for Free and 1:1. - Zoom readout: show the scale relative to the image's NATURAL pixels (fitRatio x zoomLevel), so a huge image shown whole reads as e.g. 30% instead of a misleading 100%; 100% means 1:1. Internal zoom mechanics unchanged. - Accordion panels: default to all sections open (adjust/transform/fileinfo/history) when there is no stored layout, instead of all collapsed. - Add the missing crop aspect/width/height i18n keys plus orientation labels.
🤖 Bedrock Review —
|
|
…tor state) - Address bar: add an "open in new tab" button in the URL pill that opens the current preview URL in a new tab (same absolute URL the copy action uses). - Re-add the focal point as a canvas tool: a header toggle next to crop plus a draggable marker overlay that records a normalized 0..1 point in the store (withFocalPoint reducer; not in the filter chain or history). - The focal point is intentionally NOT persisted on its own. In dotCMS the focal write only stages to a temp slot (FocalPointImageFilter prepends TMP::, committed at check-in), so it belongs to the Save flow (#36067). The marker just records the point in editor state for Save to persist later.
🤖 Bedrock Review —
|
Summary
CleanShot.2026-06-24.at.16.03.56.mp4
Replaces the legacy Dojo
ImageEditor.jswith a new standalone Angular library@dotcms/image-editor, wired into Edit Content's binary field through anIMAGE_EDITOR_LAUNCHERseam (Angular-only path).The editor is a viewer of a dotCMS endpoint: the
<img>srcis a computed/contentAsset/image/{id}/{field}/filter/...URL, so every control change rebuildsthe 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 ofone vertical
signalStoreFeatureper 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
@dotcms/image-editor: root dialog, header, canvas (crossfade, zoom +pan, crop/focal overlays), side panels (Adjust / Transform / File info / History),
footer (Cancel + Download).
(Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z, Ctrl+Y), ignored while a text field is focused.
libs/edit-content(token + Angular launcher) and binary-field wiring.focal-centered aspect crop.
Preview robustness
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
image-editor.constants.ts; types/models inmodels/image-editor.models.ts.store/features/with-*.feature.ts+store-utils.ts;store.tsis a thin composition.Testing
nx lint image-editor/nx lint edit-content— cleannx test image-editor— 258 passing (per-feature unit specs + store-utils + dimensions.util + integration; store-feature branch coverage ~100%)nx test edit-content— 1961 passing (28 skipped)Issues
/contentAsset/imagefilter URLsAngularImageEditorLaunchermodal is implemented, but true full-screen sizing is not working yet (follow-up there)