Skip to content

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

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

feat(image-editor): add Angular image editor#36236
oidacra wants to merge 15 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

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

…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
@claude

claude Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Claude finished @oidacra's task in 1m 32s —— View job


Rollback Safety Analysis

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

Result: ✅ Safe to Roll Back

This PR introduces the new Angular image editor library (@dotcms/image-editor) gated behind a feature flag (FEATURE_FLAG_NEW_IMAGE_EDITOR=false by default). All changes were checked against every rollback-unsafe category:

Category Finding
C-1 Structural Data Model Change ❌ No runonce tasks, no table/column changes
C-2 Elasticsearch Mapping Change ❌ No ES mapping or reindex changes
C-3 Content JSON Model Version Bump ❌ No CURRENT_MODEL_VERSION changes
C-4 DROP TABLE / DROP COLUMN ❌ No DDL drops
H-1 One-Way Data Migration ❌ No data transformations
H-2 RENAME TABLE / COLUMN ❌ No renames
H-3 PK Restructuring ❌ No PK changes
H-4 New Content Type Field Type ❌ No new field type registered in FieldTypeAPI
H-5 Binary Storage Provider Change ❌ No storage provider changes
H-6 DROP PROCEDURE/FUNCTION ❌ No stored procedure changes
H-7 NOT NULL column without default ❌ No DB schema changes at all
H-8 VTL Viewtool Contract Change ❌ No viewtool changes
M-1 Non-Broadening Column Type Change ❌ No column type changes
M-2 Push Publishing Bundle Format ❌ No Bundler/Handler changes
M-3 REST/GraphQL API Contract Change ❌ Only additive: new feature flag constant added to ConfigurationResource allowlist and FeatureFlagName interface — purely additive, no existing field renamed or removed
M-4 OSGi Plugin API Breakage ❌ No OSGi interface changes

What changed (backend):

  • FeatureFlagName.java: New constant FEATURE_FLAG_NEW_IMAGE_EDITOR added (additive only)
  • ConfigurationResource.java: New flag added to the feature flag allowlist (additive only)
  • dotmarketing-config.properties: New flag defaulting to false (the legacy Dojo editor remains active until explicitly enabled)
  • Language.properties: New i18n keys added (purely additive)
  • All other changes are Angular/TypeScript frontend files in core-web/libs/image-editor/ and core-web/libs/edit-content/

The feature flag defaults to false, so rolling back to N-1 simply means the new editor is never shown — N-1 ignores the unknown flag value and continues using the legacy Dojo editor. No data has been written in a new format, no schema has changed, and no API contract has been broken.

Label applied: AI: Safe To Rollback

@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 the isAvailable() check on line 130 (if (!launcher?.isAvailable())) will throw a runtime error if launcher is null. The optional injection means launcher could be undefined, but isAvailable() is called on a potentially undefined object. Should be if (!launcher || !launcher.isAvailable()).

[🟡 Medium] core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component.ts:130 — The fallback logic to the legacy Dojo editor is correct, but the openImageEditor call uses this.tempId which might be undefined when editing an existing contentlet (byInode: true). The legacy service might expect tempId only for new files. This could cause a bug in the fallback path. Ensure the parameters are valid for both new and existing assets.

[🟡 Medium] core-web/libs/edit-content/src/lib/fields/shared/image-editor-launcher/angular-image-editor.launcher.ts:35isAvailable() returns this.#enabled(). The toSignal call uses { initialValue: false }, so the feature flag defaults to false until the server responds. This is safe (defaults to disabled), but means the new editor won't be available until the flag is fetched. This is acceptable but should be noted as intentional.

[🟡 Medium] core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.ts:130 — The pendingSrc signal is set via an effect that subscribes to previewUrl$. If the preview URL changes rapidly, there's a risk of race conditions where an older preview might be promoted after a newer one. The effect uses switchMap which cancels previous requests, which is correct, but the pendingSrc update and image load event handling should be robust to out-of-order completions. The current logic with displayedUrl comparison seems adequate.

[🟡 Medium] core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.ts:454 — The component uses viewChild for displayImg and stage. The ResizeObserver is set up in an effect that depends on displayImg(). If the element reference changes (e.g., on image swap), the observer should be re-established. The current effect will re-run when displayImg changes, which is correct, but ensure cleanup of previous observer (it uses takeUntilDestroyed on the effect destroy ref, which will clean up on component destroy, but not on element change). Might need explicit unobserve in a cleanup function.

[🟡 Medium] core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.ts:200-210 — The onStagePointerDown method initiates panning. It uses setPointerCapture and listens to pointermove/pointerup on the window. This is fine, but there's no guard against multiple simultaneous pointer events (e.g., multitouch). Should ensure only one pan gesture is active at a time (check panning() signal). Currently panning is a boolean signal, but the event handlers could be triggered again while panning is true. Consider ignoring new pointerdown if panning() is already true.

[🟠 High] core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.ts:230 — The retry() method dispatches retryRequested. However, if the preview is in an error state, clicking retry should also reset the previewStatus to 'loading' or similar. The store likely handles this, but ensure the UI provides feedback (the loading spinner appears). The template shows spinner when previewStatus() === 'loading', which should be triggered by the store event.

[🟡 Medium] core-web/libs/image-editor/src/lib/components/dot-image-editor-address-bar/dot-image-editor-address-bar.component.ts:73#absoluteUrl() uses this.#document.location?.origin. If the document is not available (e.g., SSR), this could be undefined. The fallback to empty string is okay, but the resulting URL might be malformed (e.g., undefined/path). Should handle gracefully: if origin is falsy, return the relative URL as-is? The copy operation would then copy a relative path, which might be acceptable. However, the clipboard copy expects a full URL; consider throwing or showing a user-friendly error if origin is missing.

[🟡 Medium] core-web/libs/image-editor/src/lib/components/dot-image-editor-address-bar/dot-image-editor-address-bar.component.ts:50-65copyUrl() uses navigator.clipboard.writeText. This API may not be available in insecure contexts (HTTP without HTTPS) or if the user denies permission. The catch block shows an error toast, which is good. However, the error message is generic; could consider checking for DOMException and providing a more specific message (but i18n may limit this). Acceptable as is.

[🟡 Medium] core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.html:16 — The skeleton is shown when previewStatus() === 'idle' && !displayedUrl(). This is fine, but note that displayedUrl() could be a stale value from a previous edit session? The logic ensures skeleton only shows before first image loads. Acceptable.

[🟡 Medium] core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.html:35-45 — The pending image layer uses (error)="onPendingError()". If the pending image fails to load (e.g., 404), the error handler dispatches previewErrored. However, the pending image's src is an object URL from a blob; blob URLs shouldn't 404. The error would indicate the blob was invalid or corrupted. This is handled, but ensure the object URL is revoked after error to avoid memory leak. The pendingSrc effect likely revokes previous URL, but if error occurs before the next change, the URL might linger. Consider revoking in onPendingError.

[🟡 Medium] core-web/libs/image-editor/src/lib/components/dot-image-editor-canvas/dot-image-editor-canvas.component.html:100-115 — The footer band for focal tool shows aspect ratio buttons. The selectedAspect is a local signal initialized to first preset. Changing the aspect updates the focal overlay via the store? The applyFocalCrop() dispatches an event with the selected aspect. However, the focal overlay might need to update visually when aspect changes. The overlay likely listens to store state for aspect ratio. Ensure the store is updated when user selects a different aspect preset (currently only on button click). The selectedAspect.set(preset) updates the local signal but not the store. The focal overlay might rely on store state; need to dispatch a store event to update aspect ratio. Check DotImageEditorFocalOverlayComponent for input.

[🟡 Medium] core-web/libs/edit-content/src/lib/edit-content.shell.component.ts:12-25 — The DialogService is added to providers along with the IMAGE_EDITOR_LAUNCHER token. This scopes the new Angular image editor to the edit-content shell. However, note that DialogService is already provided in a higher module (likely PrimeNG module). Providing it again here creates a new instance scoped to this component and its children. This is intentional to avoid leaking into legacy paths, but could cause duplicate dialog instances if other parts of the app also provide it. Ensure there are no adverse side effects (e.g., dialogs from other components not working). Since it's a leaf component shell, likely safe.

[🟡 Medium] core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component.spec.ts:135 — The test sets up imageEditorLauncherMock.isAvailable.mockReturnValue(true). However, the mock is provided via { provide: IMAGE_EDITOR_LAUNCHER, useValue: imageEditorLauncherMock }. This is fine, but note that the mock's isAvailable is called in the component's onEditImage. The test suite covers both new and legacy paths. Good.

[🟡 Medium] core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component.spec.ts:304-306 — The test "should map asset identifiers from the contentlet when launching" checks that byInode: true is passed. However, the byInode parameter is derived from !!inode. If inode is present, byInode is true


Run: #27981924511 · tokens: in: 21192 · out: 2048 · total: 23240

@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
- 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
@oidacra oidacra force-pushed the issue-36063-image-editor-build-dotimageeditorcomponent-modal-s branch from a1d5b80 to 5f38ef3 Compare June 19, 2026 14:51
…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
@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
…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
@oidacra oidacra marked this pull request as ready for review June 22, 2026 17:14
…(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
oidacra added 2 commits June 22, 2026 16:09
- 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
@github-actions github-actions Bot added Area : Backend PR changes Java/Maven backend code and removed AI: Safe To Rollback labels Jun 22, 2026
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
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

1 participant