Skip to content

Commit e38c66d

Browse files
feat(image-editor): wire Save and focal-point persistence for the new image editor (#36350)
## Summary Wires the new Angular image editor's **Save** to the temp-file flow and completes **focal-point persistence** for the new Edit Content. On Save, the editor GETs the contentAsset filter URL it already builds for preview, with `binaryFieldId=<fieldVar>&_imageToolSaveFile=true` appended; the servlet stages the filtered render as a `DotCMSTempFile`, the dialog closes returning it, and the field stages it (preview refreshes; the temp id is sent as the field value on check-in — the source binary is never overwritten). A focal point is folded into the same request (`/filter/FocalPoint/fp/x,y` + `overwrite=true`) and committed via `copyMetadata`; the editor seeds the marker from the asset's stored focal on open and treats a focal-only change as dirty. Closes #36067. ## What changed **Image editor (`libs/image-editor`)** — new `withSave` feature + `saveRequested/saveSucceeded/saveFailed` events + `saveEditedImage` GET; `buildSaveUrl` reuses the preview filter chain, appends the save flags, and folds in the focal point (epsilon-compared against the seeded baseline — recenter-to-clear and untouched-seed both handled); the dialog closes with the temp on success and stays open + surfaces the error on failure; focal seeded on open from `ImageEditorOpenParams.focalPoint`; `isDirty` widened to detect a focal move (epsilon-tolerant); footer **Save** button (+ i18n) shows a loading spinner while the editor is busy applying a filter **or** saving. **Binary field (`libs/edit-content`)** — `onEditImage` reads the asset's stored focal (`{field}MetaData.focalPoint`, via `parseFocalPoint`) and passes it to the launcher so the marker re-seeds on reopen; null-metadata guards (`handleTempFile`, `getFileMetadata`) so the image-tool temp (`metadata: null`) renders instead of crashing. **Backend** — `BinaryExporterServlet` re-wraps the temp after copying the rendered bytes so it carries real metadata (`image`/`mimeType`/dimensions) — it was returning `metadata:null/image:false/mimeType:unknown`, which left the field showing a file icon — mirroring `TempFileAPI.createTempFile`; `DefaultTransformStrategy.addBinaries` exposes `focalPoint` on the content REST read path (mirrors `BinaryViewStrategy`) so the editor can re-seed. ## Acceptance criteria - [x] Save builds the contentAsset filter URL + `binaryFieldId` + `_imageToolSaveFile=true` and GETs it - [x] Returned temp id used; dialog closes with the temp file; field preview refreshes (on check-in) - [x] Error → dialog stays open, error surfaced, field value unchanged - [x] `binaryFieldId` is the dynamic field variable - [x] Focal point folded into the save chain + committed; seeded on open; focal-only change marks dirty - [x] `focalPoint` exposed on the content read path - [ ] Download / Cancel — verified, implemented in part 1 (#36236) - [ ] Enterprise availability gate — #36058 ## Test plan Unit: `buildSaveUrl` (filters, focal seed-vs-centre, recenter-to-clear, epsilon), `withSave` (status transitions + the built Save URL), footer Save (click, loading spinner on busy/saving), dialog close-on-success / no-close-on-failure, focal seeding, `isDirty` (epsilon), `parseFocalPoint`, null-metadata guards, `DefaultTransformStrategy` focalPoint — **334 (image-editor) + 1980 (edit-content) passing**, lint clean, backend compiles. Manual (local, `FEATURE_FLAG_NEW_IMAGE_EDITOR=true`): open the editor on a Binary field → apply a filter / move the focal point → Save → the dialog closes; after **Publish** the field shows the edited image; reopen the editor → the focal marker is restored. ## Notes - Branched off the in-flight part-1 branch (#36063, currently in the merge queue), so until that merges the diff includes its commits — rebase onto `main` once #36063 lands. - Out of scope (#36058): the new unified File field's Edit action + `setPreviewFile` consumption and the Enterprise availability gate. The committed-thumbnail "shows the old image until Publish" behaviour is the expected publish lifecycle (the field renders the committed binary), not a bug. --------- Co-authored-by: Adrian Molina <adrian.molina@dotcms.com>
1 parent 8828680 commit e38c66d

53 files changed

Lines changed: 1175 additions & 174 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

core-web/apps/dotcms-ui-e2e/src/tests/edit-content/fields/file-upload-fields/binary-field/binary-field-image-editor.spec.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@ test.describe('Binary field image editor — new editor', () => {
5656
}
5757
});
5858

59-
// The unified Binary field shows "Edit image" for image files. The new
60-
// editor opens the legacy image editor JSP inside a PrimeNG dialog iframe.
61-
test('import image and Edit opens legacy image editor dialog @critical', async ({ page }) => {
59+
// The unified Binary field shows "Edit image" for image files. In the new
60+
// Edit Content the new Angular image editor opens (the FEATURE_FLAG_NEW_IMAGE_EDITOR
61+
// gate was removed, so it is always used here; the legacy Dojo editor only runs in
62+
// the legacy Edit Content — see the describe block below).
63+
test('import image and Edit opens the new Angular image editor @critical', async ({ page }) => {
6264
const formPage = new NewEditContentFormPage(page);
6365
await formPage.goToNew(contentTypeVariable);
6466

core-web/apps/dotcms-ui-e2e/src/tests/edit-content/fields/file-upload-fields/binary-field/helpers/binary-field.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,20 +96,17 @@ export class BinaryField extends FileField {
9696
}
9797

9898
/**
99-
* New editor: the legacy image editor JSP is embedded in a PrimeNG dialog
100-
* iframe (`dot-legacy-image-editor-dialog`), not the Dojo `#dotImageDialog`.
99+
* New editor: clicking "Edit image" opens the Angular image editor
100+
* ({@link DotImageEditorComponent}, `data-testid="image-editor-root"`) inside a
101+
* PrimeNG dialog. The flag that used to gate it (FEATURE_FLAG_NEW_IMAGE_EDITOR) was
102+
* removed, so the new editor is always used in the new Edit Content — there is no
103+
* legacy Dojo iframe here.
101104
*/
102105
async expectImageEditorOpen() {
103106
const dialog = this.page.getByRole('dialog');
104107
await expect(dialog).toBeVisible({ timeout: 15000 });
105108

106-
const iframe = dialog.getByTestId('legacy-image-editor-iframe');
107-
await expect(iframe).toBeVisible({ timeout: 30000 });
108-
109-
const editorFrame = this.page.frameLocator('[data-testid="legacy-image-editor-iframe"]');
110-
await expect(editorFrame.locator('#dotImageDialog, #imageToolIframe').first()).toBeVisible({
111-
timeout: 30000
112-
});
109+
await expect(dialog.getByTestId('image-editor-root')).toBeVisible({ timeout: 30000 });
113110
}
114111

115112
async openImageEditorInNewEditor() {

core-web/libs/dotcms-models/src/lib/dot-contentlets-events.model.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { DotCMSContentType, DotPageContainer, DotPageContent } from '@dotcms/dotcms-models';
1+
import { DotCMSContentType } from './dot-content-types.model';
2+
import { DotPageContainer } from './dot-page-container.model';
3+
import { DotPageContent } from './dot-page-content.model';
24

35
export interface DotContentletEvent<T = Record<string, unknown>> {
46
name: string;

core-web/libs/dotcms-models/src/lib/dot-file-metadata.model.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,9 @@ export interface DotFileMetadata {
1111
height?: number;
1212
width?: number;
1313
editableAsText?: boolean;
14+
/**
15+
* Focal point as an `"x,y"` string (normalized 0..1), exposed by the backend on image
16+
* binary metadata and used to re-seed the image editor's focal marker on reopen.
17+
*/
18+
focalPoint?: string;
1419
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export interface DotHttpRequestOptions {
22
method: string;
33
headers: { [key: string]: string };
4-
body: any;
4+
body: XMLHttpRequestBodyInit | null;
55
}

core-web/libs/dotcms-models/src/lib/dot-workflow-action.model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export interface DotCMSSystemAction {
7070

7171
export interface DotCMSWorkflowInput {
7272
id: string;
73-
body: any;
73+
body: Record<string, unknown>;
7474
}
7575

7676
export interface DotCMSContentletWorkflowActions {

core-web/libs/dotcms-models/src/lib/page-model-change-event.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { DotPageContainer } from '@dotcms/dotcms-models';
2-
1+
import { DotPageContainer } from './dot-page-container.model';
32
import { PageModelChangeEventType } from './page-model-change-event.type';
43

54
export interface PageModelChangeEvent {

core-web/libs/dotcms-models/src/lib/shared-models.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ export const enum FeaturedFlags {
3636
FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION = 'FEATURE_FLAG_UVE_LEGACY_SCRIPT_INJECTION',
3737
FEATURE_FLAG_NEW_BLOCK_EDITOR = 'FEATURE_FLAG_NEW_BLOCK_EDITOR',
3838
FEATURE_FLAG_REPORT_ISSUE_ENABLED = 'FEATURE_FLAG_REPORT_ISSUE_ENABLED',
39-
FEATURE_FLAG_LOCALE_SELECTOR_V2 = 'FEATURE_FLAG_LOCALE_SELECTOR_V2',
40-
FEATURE_FLAG_NEW_IMAGE_EDITOR = 'FEATURE_FLAG_NEW_IMAGE_EDITOR'
39+
FEATURE_FLAG_LOCALE_SELECTOR_V2 = 'FEATURE_FLAG_LOCALE_SELECTOR_V2'
4140
}
4241

4342
export const enum DotConfigurationVariables {

core-web/libs/edit-content/src/lib/fields/dot-edit-content-calendar-field/components/calendar-field/calendar-field.util.spec.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -570,11 +570,18 @@ describe('DotEditContentCalendarFieldUtil - TDD Approach', () => {
570570
const todayInServerTz = getCurrentServerTime(SERVER_TIMEZONE_MOCKS.GULF);
571571
expect(result?.displayValue.getDate()).toBe(todayInServerTz.getDate());
572572

573-
// formValue (UTC) might be different day due to timezone conversion, allow ±2 days
574-
const expectedFormDate = todayInServerTz.getDate();
575-
const actualFormDate = result?.formValue.getDate();
576-
expect(actualFormDate).toBeDefined();
577-
expect(Math.abs(actualFormDate - expectedFormDate)).toBeLessThanOrEqual(2);
573+
// formValue (UTC) might fall on a different calendar day due to timezone
574+
// conversion; allow ±2 days. Compare the whole-day distance, not getDate()
575+
// day-of-month subtraction — the latter wraps at month boundaries (e.g.
576+
// Jun 30 vs Jul 1 would read as |30 - 1| = 29 instead of a 1-day difference).
577+
const formValue = result?.formValue;
578+
expect(formValue).toBeDefined();
579+
const dayMs = 24 * 60 * 60 * 1000;
580+
const dayAtMidnight = (date: Date) =>
581+
new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
582+
const dayDistance =
583+
Math.abs(dayAtMidnight(formValue!) - dayAtMidnight(todayInServerTz)) / dayMs;
584+
expect(dayDistance).toBeLessThanOrEqual(2);
578585
});
579586

580587
it('should handle "now" defaultValue correctly for DATE fields', () => {

core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-file-field/dot-file-field.component.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
} from './../../services/image-editor';
4949
import { DotFileFieldUploadService } from './../../services/upload-file/upload-file.service';
5050
import { FileFieldStore } from './../../store/file-field.store';
51+
import { parseFocalPoint } from './../../utils/focal-point.util';
5152
import { getUiMessage } from './../../utils/messages';
5253
import { DotFileFieldPreviewComponent } from './../dot-file-field-preview/dot-file-field-preview.component';
5354
import { DotFileFieldUiMessageComponent } from './../dot-file-field-ui-message/dot-file-field-ui-message.component';
@@ -104,10 +105,10 @@ export class DotFileFieldComponent
104105
readonly #legacyDojoImageEditorLauncher = inject(LegacyDojoImageEditorLauncher);
105106
/**
106107
* New Angular image editor launcher. Provided by the Angular edit-content shell
107-
* ({@link EditContentShellComponent}) and gated behind the
108-
* `FEATURE_FLAG_NEW_IMAGE_EDITOR` flag via its `isAvailable()`. Injected as
109-
* `{ optional: true }`: when absent (e.g. the legacy web-component host) or its
110-
* flag is off, `onEditImage()` falls back to the legacy launchers.
108+
* ({@link EditContentShellComponent}); its `isAvailable()` is always true now that
109+
* the new editor is GA in the new Edit Content. Injected as `{ optional: true }`:
110+
* when absent (e.g. the legacy web-component host), `onEditImage()` falls back to
111+
* the legacy launchers.
111112
*/
112113
readonly #imageEditorLauncher = inject(IMAGE_EDITOR_LAUNCHER, { optional: true });
113114
/**
@@ -408,13 +409,16 @@ export class DotFileFieldComponent
408409
: variable;
409410

410411
// Prefer the new Angular image editor when its launcher is provided (Angular
411-
// edit-content shell) and its feature flag is on. Otherwise fall back to the
412-
// legacy editor: Dojo DOM events for the web-component bridge, dialog iframe
413-
// elsewhere.
412+
// edit-content shell). Otherwise fall back to the legacy editor: Dojo DOM events
413+
// for the web-component bridge, dialog iframe elsewhere.
414414
const newLauncher = this.#imageEditorLauncher;
415415

416416
if (newLauncher?.isAvailable()) {
417417
const metadata = this.#currentMetadata();
418+
// Seed the editor with the asset's stored focal point (exposed on the binary
419+
// metadata as an "x,y" string by DefaultTransformStrategy) so reopening restores
420+
// the marker instead of resetting it to centre.
421+
const focalPoint = parseFocalPoint(metadata?.focalPoint);
418422

419423
this.#applyEditedImage(
420424
newLauncher.open({
@@ -424,7 +428,8 @@ export class DotFileFieldComponent
424428
fieldName: editorVariable,
425429
byInode: !!inode,
426430
fileName: this.$currentFileName() || undefined,
427-
mimeType: metadata?.contentType
431+
mimeType: metadata?.contentType,
432+
focalPoint
428433
})
429434
);
430435

0 commit comments

Comments
 (0)