Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,15 @@ test('import from URL completes without 400 and shows preview @critical', async
await field.importFromUrl(E2E_IMPORT_URL);
});

test('import image URL does not show Edit image button', async ({ page }) => {
test('import image URL shows Edit image button', async ({ page }) => {
const formPage = new NewEditContentFormPage(page);
await formPage.goToNew(contentTypeVariable);

const field = new FileField(page, FILE_FIELD_VARIABLE);
await field.expectVisible();
await field.importFromUrl(E2E_IMPORT_URL);
await field.expectEditButtonHidden();
// File fields expose the image editor when the file is an image (#36363).
await field.expectEditButtonVisible();
});

test.describe('required file field', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ export class FileField {
}

/**
* The "Edit image" action is only available for Binary fields with an image file.
* The "Edit image" action is available for Binary, Image and File fields when the
* previewed file is an image (#36363).
*/
async expectEditButtonVisible() {
await expect(this.editButton.or(this.editButtonResponsive).first()).toBeVisible({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,15 @@ test('upload an image, save, reload, and thumbnail still displayed @critical', a
await field.expectPreviewShowsFileName(TEST_IMAGE.name);
});

test('upload image does not show Edit image button', async ({ page }) => {
test('upload image shows Edit image button', async ({ page }) => {
const formPage = new NewEditContentFormPage(page);
await formPage.goToNew(contentTypeVariable);

const field = new ImageField(page, IMAGE_FIELD_VARIABLE);
await field.expectVisible();
await field.uploadFile(TEST_IMAGE);
await field.expectEditButtonHidden();
// Image fields expose the image editor for images (#36363).
await field.expectEditButtonVisible();
});

test.describe('required image field', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface DotActionRequestOptions {
action: ActionToFire;
individualPermissions?: { [key: string]: string[] };
formData?: FormData;
/** Language of the contentlet version to fire against (sent as `?language=`). */
language?: string | number;
}

export interface DotFireActionOptions<T> {
Expand Down Expand Up @@ -195,6 +197,28 @@ export class DotWorkflowActionsFireService {
});
}

/**
* Fire the default PUBLISH action against an existing contentlet, resolved by the
* `identifier` in `data` and the given language. Used to check in and publish a new
* version of the `dotAsset` referenced by an Image/File field from the image editor.
*
* @template T
* @param {{ [key: string]: string }} data contentlet fields (must include `identifier`)
* @param {string | number} [language] language of the version to fire against
* @returns {Observable<T>}
* @memberof DotWorkflowActionsFireService
*/
publishContentletByIdentifier<T>(
data: { [key: string]: string },
language?: string | number
): Observable<T> {
return this.request<T>({
data,
action: ActionToFire.PUBLISH,
language
});
}

/**
* Fire an "DELETE" action over the content type received with the specified data
*
Expand Down Expand Up @@ -239,7 +263,8 @@ export class DotWorkflowActionsFireService {
data,
action,
individualPermissions,
formData
formData,
language
}: DotActionRequestOptions): Observable<T> {
let url = `${this.BASE_URL}/actions/default/fire/${action}`;

Expand All @@ -249,6 +274,10 @@ export class DotWorkflowActionsFireService {
: { contentlet };
const params = new URLSearchParams({});

if (language !== undefined && language !== null && `${language}` !== '') {
params.append('language', `${language}`);
}

// It's not best approach but this legacy code
if (contentlet['inode']) {
params.append('inode', contentlet['inode']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,22 +162,22 @@
[closeOnEscape]="true"
[dismissableMask]="true"
[modal]="true"
[header]="metadata.title"
[header]="metadata?.title"
[appendTo]="'body'"
[style]="{ maxWidth: '582px', width: '100%' }">
<div class="dot-file-field-preview__dialog-list">
<div class="file-info__item">
<span class="file-info__label">{{ 'Size' | dm }}:</span>
<div class="file-info__value-row">
@if (metadata.width && metadata.height) {
@if (metadata?.width && metadata?.height) {
<div class="file-info__value-group" data-testId="file-resolution">
<i class="pi pi-arrows-alt"></i>
<span>{{ metadata.width }}px, {{ metadata.height }}px</span>
<span>{{ metadata?.width }}px, {{ metadata?.height }}px</span>
</div>
}
<div class="file-info__value-group">
<i class="pi pi-file"></i>
<span>{{ metadata.fileSize | dotFileSizeFormat }}</span>
<span>{{ metadata?.fileSize | dotFileSizeFormat }}</span>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ describe('DotFileFieldPreviewComponent', () => {

expect(spectator.query(byTestId('download-btn'))).toBeFalsy();
});

it('renders without crashing when the temp file has no metadata', () => {
// Regression: the image-editor Save servlet can return a temp file with
// `metadata: null`. The file-info dialog header bound `metadata.title`
// unguarded and threw "Cannot read properties of null (reading 'title')".
spectator.setInput('previewFile', {
source: 'temp',
file: { ...TEMP_FILE_MOCK, image: false, mimeType: 'unknown', metadata: null }
} as unknown);

expect(() => spectator.detectChanges()).not.toThrow();
expect(spectator.component).toBeTruthy();
});
});

describe('contentlet without content preview file', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { of } from 'rxjs';

import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { ReactiveFormsModule } from '@angular/forms';

import { DialogService } from 'primeng/dynamicdialog';

import {
DotAiService,
DotMessageService,
DotWorkflowActionsFireService
} from '@dotcms/data-access';
import { createFakeContentlet } from '@dotcms/utils-testing';

import { DotFileFieldComponent } from './dot-file-field.component';

import { BINARY_FIELD_MOCK, FILE_FIELD_MOCK, IMAGE_FIELD_MOCK } from '../../../../utils/mocks';
import {
LegacyDialogImageEditorLauncher,
LegacyDojoImageEditorLauncher
} from '../../services/image-editor';
import { DotFileFieldUploadService } from '../../services/upload-file/upload-file.service';
import { FileFieldStore } from '../../store/file-field.store';
import { DotFileFieldPreviewComponent } from '../dot-file-field-preview/dot-file-field-preview.component';
import { DotFileFieldUiMessageComponent } from '../dot-file-field-ui-message/dot-file-field-ui-message.component';

/**
* Availability of the image editor in the legacy Dojo host — i.e. when the
* Angular image-editor launcher (`IMAGE_EDITOR_LAUNCHER`) is NOT provided (the
* token is only supplied by the new Angular Edit Content shell).
*
* Binary fields keep the binary inline and fall back to the legacy editor, so
* they still expose the action. Image/File fields reference a separate dotAsset
* and are new-Edit-Content-only, so they must NOT expose the editor here.
*
* Kept in a dedicated spec because Spectator allows a single
* `createComponentFactory` per file, and this scenario needs a factory that
* omits the launcher token.
*/
describe('DotFileFieldComponent — legacy host availability (no Angular launcher)', () => {
let spectator: Spectator<DotFileFieldComponent>;

const createComponent = createComponentFactory({
component: DotFileFieldComponent,
imports: [ReactiveFormsModule],
componentMocks: [DotFileFieldPreviewComponent, DotFileFieldUiMessageComponent],
providers: [
FileFieldStore,
mockProvider(DotFileFieldUploadService),
mockProvider(DialogService),
LegacyDialogImageEditorLauncher,
LegacyDojoImageEditorLauncher,
mockProvider(DotWorkflowActionsFireService),
mockProvider(DotMessageService, { get: jest.fn().mockReturnValue('Test Message') }),
mockProvider(DotAiService, {
checkPluginInstallation: jest.fn().mockReturnValue(of(false))
}),
provideHttpClient(),
provideHttpClientTesting()
// IMAGE_EDITOR_LAUNCHER intentionally not provided (legacy host).
]
});

const setReferencedImageAsset = (field: typeof IMAGE_FIELD_MOCK) => {
spectator = createComponent({
props: {
field,
contentlet: createFakeContentlet({ [field.variable]: 'ref-identifier' }),
hasError: false
} as never
});
spectator.detectChanges();
spectator.component.store.setPreviewFile({
source: 'contentlet',
file: {
identifier: 'ref-identifier',
assetMetaData: { isImage: true, contentType: 'image/png', name: 'beach.png' }
} as never
});
spectator.detectChanges();
};

it('hides the editor for an Image field even when the asset is an image', () => {
setReferencedImageAsset(IMAGE_FIELD_MOCK);
expect(spectator.component.$canEditImage()).toBe(false);
});

it('hides the editor for a File field even when the asset is an image', () => {
setReferencedImageAsset(FILE_FIELD_MOCK);
expect(spectator.component.$canEditImage()).toBe(false);
});

it('still exposes the editor for a Binary image field (legacy fallback)', () => {
spectator = createComponent({
props: {
field: BINARY_FIELD_MOCK,
contentlet: createFakeContentlet({ [BINARY_FIELD_MOCK.variable]: null }),
hasError: false
} as never
});
spectator.detectChanges();
spectator.component.store.setPreviewFile({
source: 'temp',
file: {
id: 'temp-1',
fileName: 'img.png',
metadata: { isImage: true, contentType: 'image/png', name: 'img.png' }
}
} as never);
spectator.detectChanges();
expect(spectator.component.$canEditImage()).toBe(true);
});
});
Loading
Loading