From 796a528f95d660ee89b281d66fec329f6c1905a0 Mon Sep 17 00:00:00 2001 From: Nicolas Molina Date: Fri, 13 Sep 2024 15:36:41 -0400 Subject: [PATCH] feat(editor-content): implement the new file field #29871 --- .../dot-edit-content-field.component.html | 5 + .../dot-edit-content-field.component.ts | 4 +- .../components/dot-edit-content-form/utils.ts | 1 + .../dot-binary-field-editor.component.html | 53 -- .../dot-binary-field-editor.component.scss | 82 --- .../dot-binary-field-editor.component.spec.ts | 297 --------- .../dot-binary-field-editor.component.ts | 248 ------- .../dot-binary-field-preview.component.html | 161 ----- .../dot-binary-field-preview.component.scss | 208 ------ ...dot-binary-field-preview.component.spec.ts | 329 ---------- ...-binary-field-preview.component.stories.ts | 174 ----- .../dot-binary-field-preview.component.ts | 215 ------- ...dot-binary-field-ui-message.component.html | 7 - ...dot-binary-field-ui-message.component.scss | 36 -- ...-binary-field-ui-message.component.spec.ts | 63 -- .../dot-binary-field-ui-message.component.ts | 18 - .../dot-binary-field-url-mode.component.html | 54 -- .../dot-binary-field-url-mode.component.scss | 37 -- ...ot-binary-field-url-mode.component.spec.ts | 153 ----- .../dot-binary-field-url-mode.component.ts | 134 ---- .../store/dot-binary-field-url-mode.spec.ts | 153 ----- .../store/dot-binary-field-url-mode.store.ts | 136 ---- ...dot-edit-content-file-field.component.html | 135 +--- ...-edit-content-file-field.component.spec.ts | 604 ------------------ ...it-content-file-field.component.stories.ts | 107 ---- .../dot-edit-content-file-field.component.ts | 485 +------------- .../interfaces/index.ts | 41 -- ...ot-binary-field-edit-image.service.spec.ts | 121 ---- .../dot-binary-field-edit-image.service.ts | 94 --- ...dot-binary-field-validator.service.spec.ts | 29 - .../dot-binary-field-validator.service.ts | 50 -- .../store/file-field.store.spec.ts | 275 -------- .../store/file-field.store.ts | 258 -------- .../utils/binary-field-utils.ts | 52 -- .../dot-edit-content-file-field/utils/mock.ts | 153 ----- .../lib/models/dot-edit-content-field.enum.ts | 1 + 36 files changed, 21 insertions(+), 4952 deletions(-) delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-editor/dot-binary-field-editor.component.html delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-editor/dot-binary-field-editor.component.scss delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-editor/dot-binary-field-editor.component.spec.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-editor/dot-binary-field-editor.component.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.html delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.scss delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.spec.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.stories.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-ui-message/dot-binary-field-ui-message.component.html delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-ui-message/dot-binary-field-ui-message.component.scss delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-ui-message/dot-binary-field-ui-message.component.spec.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-ui-message/dot-binary-field-ui-message.component.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/dot-binary-field-url-mode.component.html delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/dot-binary-field-url-mode.component.scss delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/dot-binary-field-url-mode.component.spec.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/dot-binary-field-url-mode.component.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/store/dot-binary-field-url-mode.spec.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/store/dot-binary-field-url-mode.store.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.spec.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.stories.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/interfaces/index.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/service/dot-binary-field-edit-image/dot-binary-field-edit-image.service.spec.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/service/dot-binary-field-edit-image/dot-binary-field-edit-image.service.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/service/dot-binary-field-validator/dot-binary-field-validator.service.spec.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/service/dot-binary-field-validator/dot-binary-field-validator.service.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.spec.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/binary-field-utils.ts delete mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/mock.ts diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html index 9e0265812f12..63b2ef04ca2d 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html @@ -93,6 +93,11 @@ [contentlet]="contentlet" [field]="field" /> } + @case (fieldTypes.FILE) { + + } } @if (field.hint) { {{ field.hint }} diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.ts index 1fbe60b13807..a6560dd3c404 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.ts @@ -7,6 +7,7 @@ import { DotFieldRequiredDirective } from '@dotcms/ui'; import { DotEditContentBinaryFieldComponent } from '../../fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component'; import { DotEditContentFieldsModule } from '../../fields/dot-edit-content-fields.module'; +import { DotEditContentFileFieldComponent } from '../../fields/dot-edit-content-file-field/dot-edit-content-file-field.component'; import { DotEditContentKeyValueComponent } from '../../fields/dot-edit-content-key-value/dot-edit-content-key-value.component'; import { DotEditContentWYSIWYGFieldComponent } from '../../fields/dot-edit-content-wysiwyg-field/dot-edit-content-wysiwyg-field.component'; import { CALENDAR_FIELD_TYPES } from '../../models/dot-edit-content-field.constant'; @@ -31,7 +32,8 @@ import { FIELD_TYPES } from '../../models/dot-edit-content-field.enum'; BlockEditorModule, DotEditContentBinaryFieldComponent, DotEditContentKeyValueComponent, - DotEditContentWYSIWYGFieldComponent + DotEditContentWYSIWYGFieldComponent, + DotEditContentFileFieldComponent ] }) export class DotEditContentFieldComponent { diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/utils.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/utils.ts index bebd2cb78ca3..39e45a896d59 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/utils.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-form/utils.ts @@ -25,6 +25,7 @@ const defaultResolutionFn: FnResolutionValue = (contentlet, field) => */ export const resolutionValue: Record = { [FIELD_TYPES.BINARY]: defaultResolutionFn, + [FIELD_TYPES.FILE]: defaultResolutionFn, [FIELD_TYPES.BLOCK_EDITOR]: defaultResolutionFn, [FIELD_TYPES.CHECKBOX]: defaultResolutionFn, [FIELD_TYPES.CONSTANT]: defaultResolutionFn, diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-editor/dot-binary-field-editor.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-editor/dot-binary-field-editor.component.html deleted file mode 100644 index 77e6909ea875..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-editor/dot-binary-field-editor.component.html +++ /dev/null @@ -1,53 +0,0 @@ -
- @if (allowFileNameEdit) { -
- - -
- -
-
- } -
- - -
- - Mime Type: {{ mimeType }} -
-
-
- - - -
-
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-editor/dot-binary-field-editor.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-editor/dot-binary-field-editor.component.scss deleted file mode 100644 index cf8011c88d99..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-editor/dot-binary-field-editor.component.scss +++ /dev/null @@ -1,82 +0,0 @@ -@use "variables" as *; - -.binary-field__editor-container { - display: flex; - justify-content: center; - align-items: flex-start; - flex-direction: column; - flex: 1; - width: 100%; - gap: $spacing-1; -} - -.binary-field__code-editor { - border: 1px solid $color-palette-gray-400; // Input - display: block; - flex-grow: 1; - width: 100%; - min-height: 20rem; - border-radius: $border-radius-md; - overflow: auto; -} - -.binary-field__code-editor--disabled { - background-color: $color-palette-gray-200; - opacity: 0.5; - - &::ng-deep { - .monaco-mouse-cursor-text, - .overflow-guard { - cursor: not-allowed; - } - } -} - -.editor-mode__form { - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; -} - -.editor-mode__input-container { - width: 100%; - display: flex; - gap: $spacing-1; - flex-direction: column; -} - -.editor-mode__input { - width: 100%; - display: flex; - flex-direction: column; -} - -.editor-mode__actions { - width: 100%; - display: flex; - gap: $spacing-1; - align-items: center; - justify-content: flex-end; -} - -.editor-mode__helper { - display: flex; - justify-content: flex-start; - align-items: center; - gap: $spacing-1; - color: $color-palette-gray-700; - font-weight: $font-size-sm; - visibility: hidden; -} - -.editor-mode__helper--visible { - visibility: visible; -} - -.error-message { - min-height: $spacing-4; // Fix height to avoid jumping - justify-content: flex-start; - display: flex; -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-editor/dot-binary-field-editor.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-editor/dot-binary-field-editor.component.spec.ts deleted file mode 100644 index c49ae4891f2c..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-editor/dot-binary-field-editor.component.spec.ts +++ /dev/null @@ -1,297 +0,0 @@ -import { MonacoEditorComponent, MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'; -import { byTestId, createComponentFactory, Spectator } from '@ngneat/spectator'; -import { MockComponent } from 'ng-mocks'; - -import { fakeAsync, tick } from '@angular/core/testing'; - -import { ButtonModule } from 'primeng/button'; -import { InputTextModule } from 'primeng/inputtext'; - -import { DotMessageService, DotUploadService } from '@dotcms/data-access'; -import { DEFAULT_BINARY_FIELD_MONACO_CONFIG } from '@dotcms/edit-content'; -import { DotFieldValidationMessageComponent, DotMessagePipe } from '@dotcms/ui'; - -import { DotBinaryFieldEditorComponent } from './dot-binary-field-editor.component'; - -import { DotBinaryFieldValidatorService } from '../../service/dot-binary-field-validator/dot-binary-field-validator.service'; -import { TEMP_FILE_MOCK } from '../../store/file-field.store.spec'; -import { CONTENTTYPE_FIELDS_MESSAGE_MOCK } from '../../utils/mock'; - -const EDITOR_MOCK = { - updateOptions: (_options) => { - /* noops */ - }, - addCommand: () => { - /* noops */ - }, - createContextKey: () => { - /* noops */ - }, - addAction: () => { - /* noops */ - }, - getOption: () => { - /* noops */ - } -} as unknown; - -globalThis.monaco = { - languages: { - getLanguages: () => { - return [ - { - id: 'javascript', - extensions: ['.js'], - mimetypes: ['text/javascript'] - } - ]; - } - } -} as typeof monaco; - -describe('DotBinaryFieldEditorComponent', () => { - let component: DotBinaryFieldEditorComponent; - let spectator: Spectator; - - let dotBinaryFieldValidatorService: DotBinaryFieldValidatorService; - - let dotUploadService: DotUploadService; - - const createComponent = createComponentFactory({ - component: DotBinaryFieldEditorComponent, - declarations: [MockComponent(MonacoEditorComponent)], - imports: [ - MonacoEditorModule, - InputTextModule, - ButtonModule, - DotMessagePipe, - DotFieldValidationMessageComponent - ], - providers: [ - DotBinaryFieldValidatorService, - { - provide: DotUploadService, - useValue: { - uploadFile: ({ file }) => { - return new Promise((resolve) => { - if (file) { - resolve(TEMP_FILE_MOCK); - } - }); - } - } - }, - { - provide: DotMessageService, - useValue: CONTENTTYPE_FIELDS_MESSAGE_MOCK - } - ] - }); - - beforeEach(() => { - spectator = createComponent({ - detectChanges: false, - props: { - allowFileNameEdit: true - } - }); - - component = spectator.component; - component.editorRef.editor = EDITOR_MOCK as monaco.editor.IStandaloneCodeEditor; - dotUploadService = spectator.inject(DotUploadService, true); - dotBinaryFieldValidatorService = spectator.inject(DotBinaryFieldValidatorService); - dotBinaryFieldValidatorService.setAccept(['image/*', '.ts']); - - spectator.detectChanges(); - }); - - it('should emit cancel event on escape keydown', () => { - const event = new KeyboardEvent('keydown', { key: 'Escape' }); - - const cancelSpy = jest.spyOn(spectator.component.cancel, 'emit'); - const preventDefaultSpy = jest.spyOn(event, 'preventDefault'); - const stopPropagationSpy = jest.spyOn(event, 'stopPropagation'); - - document.dispatchEvent(event); - - expect(cancelSpy).toHaveBeenCalled(); - expect(preventDefaultSpy).toHaveBeenCalled(); - expect(stopPropagationSpy).toHaveBeenCalled(); - }); - - describe('input', () => { - it('should set label and have css class required', () => { - const label = spectator.query(byTestId('editor-label')); - - expect(label.innerHTML.trim()).toBe('File Name'); - expect(label.className).toBe('p-label-input-required'); - }); - - it('should show the file name editor', () => { - const input = spectator.query(byTestId('editor-file-name')); - - expect(input).not.toBeNull(); - }); - - it('should not show the file name editor', () => { - spectator.setInput('allowFileNameEdit', false); - spectator.detectChanges(); - - const input = spectator.query(byTestId('editor-file-name')); - - expect(input).toBeNull(); - }); - }); - - describe('Editor', () => { - it('should set editor language', fakeAsync(() => { - const expectedMonacoOptions = { - ...DEFAULT_BINARY_FIELD_MONACO_CONFIG, - language: 'javascript' - }; - - spectator.detectChanges(); - - component.form.setValue({ - name: 'script.js', - content: 'test' - }); - - spectator.detectComponentChanges(); - - tick(355); //due to debounceTime - - expect(component.monacoOptions()).toEqual(expectedMonacoOptions); - expect(component.mimeType).toBe('text/javascript'); - })); - it('should force html language on vtl files', fakeAsync(() => { - const expectedMonacoOptions = { - ...DEFAULT_BINARY_FIELD_MONACO_CONFIG, - language: 'html' - }; - - spectator.detectChanges(); - - component.form.setValue({ - name: 'banner.vtl', - content: 'test' - }); - - spectator.detectComponentChanges(); - - tick(355); - - expect(component.monacoOptions()).toEqual(expectedMonacoOptions); - expect(component.mimeType).toBe('text/x-velocity'); - })); - it('should fallback with plain text if language is not found', fakeAsync(() => { - const expectedMonacoOptions = { - ...DEFAULT_BINARY_FIELD_MONACO_CONFIG, - language: 'text' - }; - - spectator.detectChanges(); - - component.form.setValue({ - name: 'script.rb', - content: 'test' - }); - - spectator.detectComponentChanges(); - - tick(355); - - expect(component.monacoOptions()).toEqual(expectedMonacoOptions); - expect(component.mimeType).toBe('plain/text'); - })); - it('should emit cancel event when cancel button is clicked', () => { - const spy = jest.spyOn(component.cancel, 'emit'); - const cancelBtn = spectator.query(byTestId('cancel-button')); - - spectator.click(cancelBtn); - - expect(spy).toHaveBeenCalled(); - }); - - it('should emit tempFileUploaded event when import button is clicked if form is valid', () => { - const spy = jest.spyOn(component.tempFileUploaded, 'emit'); - const spyFormDisabled = jest.spyOn(component.form, 'disable'); - const spyFormEnabled = jest.spyOn(component.form, 'enable'); - const spyFileUpload = jest - .spyOn(dotUploadService, 'uploadFile') - .mockReturnValue(Promise.resolve(TEMP_FILE_MOCK)); - const importBtn = spectator.query('[data-testId="import-button"] button'); - const monacoEditor = spectator.query(MonacoEditorComponent); - monacoEditor.init.emit(); - - component.form.setValue({ - name: 'file-name.ts', - content: 'test' - }); - - spectator.click(importBtn); - - expect(spy).toHaveBeenCalledWith(TEMP_FILE_MOCK); - expect(spyFileUpload).toHaveBeenCalled(); - expect(spyFormDisabled).toHaveBeenCalled(); - expect(spyFormEnabled).toHaveBeenCalled(); - }); - - it('should not emit tempFileUploaded event when import button is clicked if form is invalid', () => { - const spy = jest.spyOn(component.tempFileUploaded, 'emit'); - const spyFormDisabled = jest.spyOn(component.form, 'disable'); - const spyFormEnabled = jest.spyOn(component.form, 'enable'); - const spyFileUpload = jest - .spyOn(dotUploadService, 'uploadFile') - .mockReturnValue(Promise.resolve(TEMP_FILE_MOCK)); - const importBtn = spectator.query('[data-testId="import-button"] button'); - - component.form.setValue({ - name: '', - content: '' - }); - - spectator.click(importBtn); - - expect(spyFileUpload).not.toHaveBeenCalled(); - expect(spyFormDisabled).not.toHaveBeenCalled(); - expect(spyFormEnabled).not.toHaveBeenCalled(); - expect(spy).not.toHaveBeenCalled(); - }); - - it('should mark name control as dirty when import button is clicked and name control is invalid', () => { - const spyDirty = jest.spyOn(component.form.get('name'), 'markAsDirty'); - const spyDdateValueAndValidity = jest.spyOn( - component.form.get('name'), - 'updateValueAndValidity' - ); - const importBtn = spectator.query('[data-testId="import-button"] button'); - - spectator.click(importBtn); - - expect(spyDirty).toHaveBeenCalled(); - expect(spyDdateValueAndValidity).toHaveBeenCalled(); - }); - - it('should set form as invalid when accept is not valid', fakeAsync(() => { - const spy = jest.spyOn(component.name, 'setErrors'); - - component.form.setValue({ - name: 'test.ts', - content: 'test' - }); - - tick(1000); - - expect(spy).toHaveBeenCalledWith({ - invalidExtension: - 'This type of file is not supported. Please use a image/*, .ts file.' - }); - expect(component.form.valid).toBe(false); - })); - - afterEach(() => { - jest.restoreAllMocks(); - }); - }); -}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-editor/dot-binary-field-editor.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-editor/dot-binary-field-editor.component.ts deleted file mode 100644 index c826cfdbbed1..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-editor/dot-binary-field-editor.component.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { - MonacoEditorComponent, - MonacoEditorConstructionOptions, - MonacoEditorModule -} from '@materia-ui/ngx-monaco-editor'; -import { from } from 'rxjs'; - -import { CommonModule } from '@angular/common'; -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - computed, - EventEmitter, - HostListener, - inject, - Input, - OnChanges, - OnInit, - Output, - signal, - ViewChild -} from '@angular/core'; -import { - FormControl, - FormGroup, - FormsModule, - ReactiveFormsModule, - Validators -} from '@angular/forms'; - -import { ButtonModule } from 'primeng/button'; -import { InputTextModule } from 'primeng/inputtext'; - -import { debounceTime } from 'rxjs/operators'; - -import { DotMessageService, DotUploadService } from '@dotcms/data-access'; -import { DotCMSTempFile } from '@dotcms/dotcms-models'; -import { DEFAULT_BINARY_FIELD_MONACO_CONFIG } from '@dotcms/edit-content'; -import { DotFieldValidationMessageComponent, DotMessagePipe } from '@dotcms/ui'; - -import { DotBinaryFieldValidatorService } from '../../service/dot-binary-field-validator/dot-binary-field-validator.service'; - -const DEFAULT_FILE_TYPE = 'text'; -@Component({ - selector: 'dot-binary-field-editor', - standalone: true, - imports: [ - CommonModule, - MonacoEditorModule, - FormsModule, - ReactiveFormsModule, - InputTextModule, - ButtonModule, - DotMessagePipe, - DotFieldValidationMessageComponent - ], - templateUrl: './dot-binary-field-editor.component.html', - styleUrls: ['./dot-binary-field-editor.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class DotBinaryFieldEditorComponent implements OnInit, OnChanges { - @Input() fileName = ''; - @Input() fileContent = ''; - @Input() allowFileNameEdit = true; - - @Output() readonly tempFileUploaded = new EventEmitter(); - @Output() readonly cancel = new EventEmitter(); - @ViewChild('editorRef', { static: true }) editorRef!: MonacoEditorComponent; - readonly form = new FormGroup({ - name: new FormControl('', [Validators.required, Validators.pattern(/^[^.]+\.[^.]+$/)]), - content: new FormControl('') - }); - mimeType = ''; - private readonly languageType = signal(DEFAULT_FILE_TYPE); - private readonly cd: ChangeDetectorRef = inject(ChangeDetectorRef); - private readonly dotUploadService: DotUploadService = inject(DotUploadService); - private readonly dotMessageService: DotMessageService = inject(DotMessageService); - private readonly dotBinaryFieldValidatorService: DotBinaryFieldValidatorService = inject( - DotBinaryFieldValidatorService - ); - private extension = ''; - private invalidFileMessage = ''; - private editor: monaco.editor.IStandaloneCodeEditor; - - private _userMonacoOptions = signal({}); - - monacoOptions = computed(() => { - return { - ...DEFAULT_BINARY_FIELD_MONACO_CONFIG, - ...this._userMonacoOptions(), - language: this.languageType() - }; - }); - - @Input() - set userMonacoOptions(customMonacoOptions: MonacoEditorConstructionOptions) { - this._userMonacoOptions.set(customMonacoOptions); - } - - get name(): FormControl { - return this.form.get('name') as FormControl; - } - - get content(): FormControl { - return this.form.get('content') as FormControl; - } - - /** - * Close the editor when the user press ESC - * And prevent the default behavior of the edit conten iframe - * - * @param {*} event - * @memberof DotBinaryFieldEditorComponent - */ - @HostListener('document:keydown.escape', ['$event']) onEscape(event) { - this.cancel.emit(); - event.preventDefault(); - event.stopPropagation(); - } - - ngOnInit(): void { - this.setFormValues(); - this.name.valueChanges - .pipe(debounceTime(350)) - .subscribe((name) => this.setEditorLanguage(name)); - this.invalidFileMessage = this.dotMessageService.get( - 'dot.binary.field.error.type.file.not.supported.message', - this.dotBinaryFieldValidatorService.accept.join(', ') - ); - } - - ngOnChanges(): void { - this.setFormValues(); - if (window.monaco) { - this.setEditorLanguage(this.fileName); - } - } - - onEditorInit() { - this.editor = this.editorRef.editor; - if (this.fileName) { - this.setEditorLanguage(this.fileName); - } - } - - onSubmit(): void { - if (this.name.invalid) { - this.markControlInvalid(this.name); - - return; - } - - const file = new File([this.content.value], this.name.value, { - type: this.mimeType - }); - this.uploadFile(file); - } - - private setFormValues(): void { - this.name.setValue(this.fileName); - this.content.setValue(this.fileContent); - } - - private markControlInvalid(control: FormControl): void { - control.markAsDirty(); - control.updateValueAndValidity(); - this.cd.detectChanges(); - } - - private uploadFile(file: File) { - const obs$ = from(this.dotUploadService.uploadFile({ file })); - this.disableEditor(); - obs$.subscribe((tempFile) => { - this.enableEditor(); - this.tempFileUploaded.emit({ - ...tempFile, - content: this.content.value - }); - }); - } - - private setEditorLanguage(fileName = '') { - const fileExtension = this.extractFileExtension(fileName); - - if (fileExtension === 'vtl') { - this.setVelocityLanguage(); - } else { - this.updateLanguageForFileExtension(fileExtension); - } - - this.validateFileType(fileExtension); - this.cd.detectChanges(); - } - - private extractFileExtension(fileName: string) { - return fileName?.includes('.') ? fileName.split('.').pop() : ''; - } - - private setVelocityLanguage() { - this.mimeType = 'text/x-velocity'; - this.extension = 'vtl'; - this.updateEditorLanguage('html'); //Force html highlighting for .vtl files - } - - private updateLanguageForFileExtension(fileExtension: string) { - const { id, mimetypes, extensions } = this.getLanguage(fileExtension) || {}; - this.mimeType = mimetypes?.[0] || 'plain/text'; - this.extension = extensions?.[0] || 'txt'; - this.updateEditorLanguage(id); - } - - private validateFileType(fileExtension: string) { - const isValidType = this.dotBinaryFieldValidatorService.isValidType({ - extension: this.extension, - mimeType: this.mimeType - }); - - if (fileExtension && !isValidType) { - this.name.setErrors({ invalidExtension: this.invalidFileMessage }); - } - } - - private getLanguage(fileExtension: string) { - // Global Object Defined by Monaco Editor - return monaco.languages - .getLanguages() - .find((language) => language.extensions?.includes(`.${fileExtension}`)); - } - - private updateEditorLanguage(languageId = 'text') { - this.languageType.set(languageId); - } - - private disableEditor() { - this.form.disable(); - this.editor.updateOptions({ - readOnly: true - }); - } - - private enableEditor() { - this.form.enable(); - this.editor.updateOptions({ - readOnly: false - }); - } -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.html deleted file mode 100644 index 9aea3678a186..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.html +++ /dev/null @@ -1,161 +0,0 @@ -
- @if (this.metadata?.editableAsText) { -
- {{ content() }} -
- } @else { -
- @if (tempFile) { - - } @else { - - } -
- - } - - - - - - -
- - -
- {{ 'Size' | dm }}: - -
- - @if (contentlet) { - @for (sourceLink of this.resourceLinks(); track $index) { - @if (sourceLink.show) { - - } - } @empty { - @for (item of [1, 2, 3, 4]; track $index) { -
- - -
- } - } - } - - - - -
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.scss deleted file mode 100644 index 0d843091f34d..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.scss +++ /dev/null @@ -1,208 +0,0 @@ -@use "variables" as *; - -:host { - display: block; - width: 100%; - height: 100%; -} - -dot-contentlet-thumbnail::ng-deep { - .background-image:not(.svg-thumbnail) { - img { - object-fit: cover; - } - } - - img { - object-fit: contain; - } -} - -.preview-container { - display: flex; - gap: $spacing-1; - align-items: flex-start; - justify-content: center; - height: 100%; - width: 100%; - position: relative; - container-type: inline-size; - container-name: preview; - - &:only-child { - gap: 0; // Remove gap if only one preview - } -} - -.preview-code_container { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - width: 100%; - user-select: none; - cursor: pointer; -} - -.preview-image__container, -dot-contentlet-thumbnail { - height: 100%; - width: 100%; - display: flex; - justify-content: center; - align-items: center; - background: $color-palette-gray-200; -} - -.preview-container--fade::after { - content: ""; - background: linear-gradient(0deg, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); - position: absolute; - width: 100%; - height: 50%; - bottom: 0; - left: 0; - border-radius: $border-radius-md; - pointer-events: none; -} - -.preview-metadata__container { - flex-grow: 1; - padding: $spacing-1; - padding-right: $spacing-6; - display: none; - flex-direction: column; - overflow: hidden; - gap: $spacing-2; - min-width: 150px; - - span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .preview-metadata_header { - font-size: $font-size-md; - font-weight: $font-weight-semi-bold; - margin: 0; - color: $black; - } -} - -.preview-metadata { - display: flex; - justify-content: flex-start; - align-items: center; - gap: $spacing-0; -} - -.preview-resource-links__actions { - position: absolute; - top: 0; - right: 0; - display: flex; - flex-direction: column; - gap: $spacing-0; - padding-top: $spacing-1; - display: none; -} - -.preview-metadata__actions { - position: absolute; - bottom: $spacing-1; - right: 0; - display: none; - justify-content: flex-end; - align-items: center; - gap: $spacing-1; - z-index: 100; -} - -.preview-metadata__action--responsive { - position: absolute; - bottom: $spacing-1; - right: $spacing-1; - display: flex; - flex-direction: column; - gap: $spacing-1; -} - -code { - background: $white; - color: $color-palette-primary-500; - height: 100%; - width: 100%; - white-space: pre-wrap; - overflow: hidden; - line-height: normal; -} - -.file-info__item { - display: flex; - padding: $spacing-0 0; - flex-direction: column; - justify-content: center; - align-items: flex-start; - gap: $spacing-0; - - &:not(:last-child)::after { - content: ""; - display: block; - width: 100%; - height: 1px; - background: $color-palette-gray-200; - margin: $spacing-1 0; - } -} - -.file-info__link { - display: flex; - align-items: center; - gap: $spacing-1; - min-height: 32px; - font-size: $font-size-sm; - width: 100%; - - a { - color: $black; - text-decoration: none; - flex: 1 0 0; - } -} - -.file-info__title { - font-size: $font-size-sm; - font-style: normal; - font-weight: 600; -} - -.file-info__size { - display: flex; - align-items: center; - gap: $spacing-0; -} - -@container preview (min-width: 376px) { - .preview-metadata__container, - .preview-metadata__actions { - display: flex; - } - - .preview-metadata__action--responsive { - display: none; - } - - .preview-image__container { - height: 100%; - max-width: 17.5rem; - } - - .preview-resource-links__actions { - display: flex; - } - - .preview-overlay__container { - display: none; - } -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.spec.ts deleted file mode 100644 index 1f1de1f24c9b..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.spec.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { Spectator, byTestId, createComponentFactory } from '@ngneat/spectator/jest'; -import { of } from 'rxjs'; - -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { fakeAsync, tick } from '@angular/core/testing'; - -import { delay } from 'rxjs/operators'; - -import { DotResourceLinksService } from '@dotcms/data-access'; - -import { DotBinaryFieldPreviewComponent } from './dot-binary-field-preview.component'; - -import { BINARY_FIELD_CONTENTLET } from '../../../../utils/mocks'; -import { TEMP_FILES_MOCK } from '../../utils/mock'; - -const CONTENTLET_MOCK = { - ...BINARY_FIELD_CONTENTLET, - baseType: 'FILEASSET', - fieldVariable: 'Binary' -}; - -const CONTENTLET_HTMLPAGE_MOCK = { - ...BINARY_FIELD_CONTENTLET, - baseType: 'HTMLPAGE', - fieldVariable: 'Binary' -}; - -const CONTENTLET_TEXT_MOCK = { - ...BINARY_FIELD_CONTENTLET, - BinaryMetaData: { - ...BINARY_FIELD_CONTENTLET.binaryMetaData, - editableAsText: true, - contentType: 'text/plain' - }, - fieldVariable: 'Binary', - content: 'Data' -}; - -const clickOnInfoButton = (spectator: Spectator) => { - const infoButton = spectator.query(byTestId('info-btn')); - spectator.click(infoButton); - spectator.detectChanges(); -}; - -describe('DotBinaryFieldPreviewComponent', () => { - let spectator: Spectator; - let dotResourceLinksService: DotResourceLinksService; - - const createComponent = createComponentFactory({ - component: DotBinaryFieldPreviewComponent, - imports: [HttpClientTestingModule], - providers: [ - { - provide: DotResourceLinksService, - useValue: { - getFileResourceLinks: () => of({}) - } - } - ] - }); - - beforeEach(() => { - spectator = createComponent({ - props: { - contentlet: CONTENTLET_MOCK, - fieldVariable: 'Binary', - tempFile: null, - editableImage: true - }, - detectChanges: false - }); - - dotResourceLinksService = spectator.inject(DotResourceLinksService, true); - }); - - it('should show contentlet thumbnail', () => { - spectator.detectChanges(); - const thumbnail = spectator.query(byTestId('contentlet-thumbnail')) as Element; - expect(thumbnail).toBeTruthy(); - expect(thumbnail['fieldVariable']).toBe(CONTENTLET_MOCK.fieldVariable); - expect(thumbnail['contentlet']).toEqual(CONTENTLET_MOCK); - }); - - it('should show temp file thumbnail', () => { - spectator.setInput('tempFile', TEMP_FILES_MOCK[0]); - spectator.setInput('contentlet', null); - - spectator.detectChanges(); - expect(spectator.query(byTestId('temp-file-thumbnail'))).toBeTruthy(); - }); - - it('should emit removeFile event when remove button is clicked', () => { - const spy = jest.spyOn(spectator.component.removeFile, 'emit'); - const removeButton = spectator.query(byTestId('remove-button')); - spectator.click(removeButton); - expect(spy).toHaveBeenCalled(); - }); - - it('should show download button', () => { - spectator.detectChanges(); - const downloadButton = spectator.query(byTestId('download-btn')); - const spyWindowOpen = jest.spyOn(window, 'open').mockImplementation(() => null); - - expect(downloadButton).toBeTruthy(); - - spectator.click(downloadButton); - spectator.detectChanges(); - - expect(spyWindowOpen).toHaveBeenCalledWith( - `/contentAsset/raw-data/${CONTENTLET_MOCK.inode}/${CONTENTLET_MOCK.fieldVariable}?byInode=true&force_download=true`, - '_self' - ); - }); - - it("should doesn't show download button", () => { - spectator.setInput('tempFile', TEMP_FILES_MOCK[0]); - spectator.setInput('contentlet', null); - spectator.detectChanges(); - const downloadButton = spectator.query(byTestId('download-btn')); - - expect(downloadButton).toBeNull(); - }); - - it('should be editable', () => { - spectator.detectChanges(); - const editButton = spectator.query(byTestId('edit-button')); - expect(editButton).toBeTruthy(); - }); - - it('should show download button responsive', () => { - spectator.detectChanges(); - const downloadButtonResponsive = spectator.query(byTestId('download-btn-responsive')); - const spyWindowOpen = jest.spyOn(window, 'open').mockImplementation(() => null); - - expect(downloadButtonResponsive).toBeTruthy(); - - spectator.click(downloadButtonResponsive); - spectator.detectChanges(); - - expect(spyWindowOpen).toHaveBeenCalledWith( - `/contentAsset/raw-data/${CONTENTLET_MOCK.inode}/${CONTENTLET_MOCK.fieldVariable}?byInode=true&force_download=true`, - '_self' - ); - }); - - describe('onEdit', () => { - describe('when file is an image', () => { - it('should emit editImage event', () => { - spectator.detectChanges(); - const spy = jest.spyOn(spectator.component.editImage, 'emit'); - const editButton = spectator.query(byTestId('edit-button')); - spectator.click(editButton); - expect(spy).toHaveBeenCalled(); - }); - }); - - describe('when contentelt is a text file', () => { - beforeEach(() => { - spectator.setInput('contentlet', CONTENTLET_TEXT_MOCK); - spectator.detectChanges(); - }); - - it('should emit editFile event when edit button is clicked', () => { - const spy = jest.spyOn(spectator.component.editFile, 'emit'); - const editButton = spectator.query(byTestId('edit-button')); - spectator.click(editButton); - expect(spy).toHaveBeenCalled(); - }); - - it('should emit editFile event click on the code preview', () => { - const spy = jest.spyOn(spectator.component.editFile, 'emit'); - const codePreview = spectator.query(byTestId('code-preview')); - spectator.click(codePreview); - expect(spy).toHaveBeenCalled(); - }); - }); - }); - - describe('editableImage', () => { - describe('when is true', () => { - it('should set isEditable to true', () => { - spectator.detectChanges(); - const editButton = spectator.query(byTestId('edit-button')); - expect(editButton).toBeTruthy(); - }); - }); - - describe('when is false', () => { - beforeEach(async () => { - spectator.setInput('editableImage', false); - spectator.detectChanges(); - await spectator.fixture.whenStable(); - }); - - it('should set isEditable to false', () => { - const editButton = spectator.query(byTestId('edit-button')); - expect(editButton).not.toBeTruthy(); - }); - }); - }); - - describe('responsive', () => { - it('should emit removeFile event when remove button is clicked', () => { - const spy = jest.spyOn(spectator.component.removeFile, 'emit'); - const removeButton = spectator.query(byTestId('remove-button-responsive')); - spectator.click(removeButton); - expect(spy).toHaveBeenCalled(); - }); - - describe('onEdit', () => { - describe('when file is an image', () => { - it('should emit editImage event', () => { - spectator.detectChanges(); - const spy = jest.spyOn(spectator.component.editImage, 'emit'); - const editButton = spectator.query(byTestId('edit-button-responsive')); - spectator.click(editButton); - expect(spy).toHaveBeenCalled(); - }); - }); - - describe('when the contentlet is a text file', () => { - beforeEach(() => { - spectator.setInput('contentlet', CONTENTLET_TEXT_MOCK); - spectator.detectChanges(); - }); - - it('should emit editFile event when edit button is clicked', () => { - const spy = jest.spyOn(spectator.component.editFile, 'emit'); - const editButton = spectator.query(byTestId('edit-button-responsive')); - spectator.click(editButton); - expect(spy).toHaveBeenCalled(); - }); - }); - }); - }); - - describe('Resource Links', () => { - const RESOURCE_LINKS = { - configuredImageURL: '/configuredImageURL', - text: '/text', - versionPath: '/versionPath', - idPath: '/idPath', - mimeType: 'image/png' - }; - - it('should have the correct resource links', () => { - const spyResourceLinks = jest - .spyOn(dotResourceLinksService, 'getFileResourceLinks') - .mockReturnValue(of(RESOURCE_LINKS)); - - spectator.detectChanges(); - - clickOnInfoButton(spectator); - - const fileLinkElement = spectator.query(byTestId('resource-link-FileLink')); - const resourceLinkElement = spectator.query(byTestId('resource-link-Resource-Link')); - const versionPathElement = spectator.query(byTestId('resource-link-VersionPath')); - const idPathElement = spectator.query(byTestId('resource-link-IdPath')); - - expect(fileLinkElement).not.toBeNull(); - expect(resourceLinkElement).not.toBeNull(); - expect(versionPathElement).not.toBeNull(); - expect(idPathElement).not.toBeNull(); - - expect(spyResourceLinks).toHaveBeenCalledWith({ - fieldVariable: 'Binary', - inodeOrIdentifier: CONTENTLET_MOCK.identifier - }); - }); - - it('should not have the Resource-Link', () => { - const spyResourceLinks = jest - .spyOn(dotResourceLinksService, 'getFileResourceLinks') - .mockReturnValue(of(RESOURCE_LINKS)); - spectator.setInput('contentlet', CONTENTLET_HTMLPAGE_MOCK); - - spectator.detectChanges(); - - clickOnInfoButton(spectator); - - const resourceLinkElement = spectator.query(byTestId('resource-link-Resource-Link')); - - expect(resourceLinkElement).toBeNull(); - expect(spyResourceLinks).toHaveBeenCalledWith({ - fieldVariable: 'Binary', - inodeOrIdentifier: CONTENTLET_MOCK.identifier - }); - }); - - it('should have the loading state', fakeAsync(() => { - const spyResourceLinks = jest - .spyOn(dotResourceLinksService, 'getFileResourceLinks') - .mockReturnValue(of(RESOURCE_LINKS).pipe(delay(1000))); - - spectator.detectChanges(); - - clickOnInfoButton(spectator); - - const loadingElements = spectator.queryAll('.file-info__loading'); - - expect(loadingElements.length).toBe(4); - - tick(1000); - - expect(spyResourceLinks).toHaveBeenCalledWith({ - fieldVariable: 'Binary', - inodeOrIdentifier: CONTENTLET_MOCK.identifier - }); - })); - - it('should not show file resolution', () => { - spectator.setInput('contentlet', { - ...CONTENTLET_MOCK, - BinaryMetaData: { - ...BINARY_FIELD_CONTENTLET.binaryMetaData, - height: 0, - width: 0 - } - }); - - spectator.detectChanges(); - - clickOnInfoButton(spectator); - - const resolution = spectator.query(byTestId('file-resolution')); - expect(resolution).toBeNull(); - }); - }); -}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.stories.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.stories.ts deleted file mode 100644 index 2cb96498e672..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.stories.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { action } from '@storybook/addon-actions'; -import { moduleMetadata, StoryObj, Meta } from '@storybook/angular'; - -import { CommonModule } from '@angular/common'; -import { HttpClientModule } from '@angular/common/http'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - -import { ButtonModule } from 'primeng/button'; -import { DialogModule } from 'primeng/dialog'; -import { SkeletonModule } from 'primeng/skeleton'; - -import { DotResourceLinksService } from '@dotcms/data-access'; -import { - DotTempFileThumbnailComponent, - DotSpinnerModule, - DotCopyButtonComponent, - DotFileSizeFormatPipe, - DotMessagePipe -} from '@dotcms/ui'; - -import { DotBinaryFieldPreviewComponent } from './dot-binary-field-preview.component'; - -import { DotFilePreview } from '../../interfaces'; -import { fileMetaData } from '../../utils/mock'; - -const previewImage: DotFilePreview = { - ...fileMetaData, - id: '123', - inode: '123', - titleImage: 'Assets', - contentType: 'image/png', - name: 'test.png' -}; - -const previewVideo = { - type: 'image', - fileSize: 8000, - content: '', - mimeType: 'video/png', - inode: '123456789', - titlevideo: 'true', - name: 'video.jpg', - title: 'video.jpg', - - contentType: 'video/png' -}; - -const previewFile = { - type: 'file', - fileSize: 8000, - mimeType: 'text/html', - inode: '123456789', - titlevideo: 'true', - name: 'template.html', - title: 'template.html', - - contentType: 'text/html', - content: ` - - - - - Document - - -

I have styles

- - ` -}; - -type Args = DotBinaryFieldPreviewComponent & { - file: DotFilePreview; - variableName: string; - fieldVariable: string; - styles: string[]; -}; - -const meta: Meta = { - title: 'Library / Edit Content / Binary Field / Components / Preview', - component: DotBinaryFieldPreviewComponent, - decorators: [ - moduleMetadata({ - imports: [ - BrowserAnimationsModule, - CommonModule, - ButtonModule, - SkeletonModule, - DotTempFileThumbnailComponent, - DotSpinnerModule, - DialogModule, - DotMessagePipe, - DotFileSizeFormatPipe, - DotCopyButtonComponent, - HttpClientModule - ], - providers: [DotResourceLinksService] - }) - ], - parameters: { - actions: { - handles: ['editFile', 'removeFile'] - } - }, - args: { - file: previewImage, - variableName: 'binaryField', - styles: [ - ` - .container { - width: 100%; - max-width: 36rem; - height: 12.5rem; - border: 1px solid #f2f2f2; - border-radius: 4px; - padding: 0.5rem; - } - ` - ] - }, - argTypes: { - file: { - defaultValue: previewImage, - control: 'object', - description: 'Preview object' - }, - variableName: { - defaultValue: 'binaryField', - control: 'text', - description: 'Field variable name' - }, - fieldVariable: { - defaultValue: 'Blog', - control: 'text', - description: 'Field variable name' - } - }, - render: (args) => ({ - props: { - ...args, - editFile: action('editFile'), - removeFile: action('removeFile') - }, - template: ` -
- -
` - }) -}; -export default meta; - -type Story = StoryObj; - -export const Image: Story = {}; - -export const Video = { - args: { - file: previewVideo, - variableName: 'binaryField', - fieldVariable: 'Blog' - } -}; - -export const File = { - args: { - file: previewFile, - variableName: 'binaryField', - fieldVariable: 'Blog' - } -}; diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.ts deleted file mode 100644 index 4b4773926979..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-preview/dot-binary-field-preview.component.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { of } from 'rxjs'; - -import { CommonModule } from '@angular/common'; -import { - CUSTOM_ELEMENTS_SCHEMA, - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - OnChanges, - OnInit, - Output, - SimpleChanges, - inject, - signal -} from '@angular/core'; - -import { ButtonModule } from 'primeng/button'; -import { DialogModule } from 'primeng/dialog'; -import { SkeletonModule } from 'primeng/skeleton'; - -import { catchError } from 'rxjs/operators'; - -import { DotResourceLinksService } from '@dotcms/data-access'; -import { - DotCMSBaseTypesContentTypes, - DotCMSContentlet, - DotCMSTempFile, - DotFileMetadata -} from '@dotcms/dotcms-models'; -import { - DotTempFileThumbnailComponent, - DotFileSizeFormatPipe, - DotMessagePipe, - DotSpinnerModule, - DotCopyButtonComponent -} from '@dotcms/ui'; - -import { getFileMetadata } from '../../utils/binary-field-utils'; - -export enum EDITABLE_FILE { - image = 'image', - text = 'text', - unknown = 'unknown' -} - -interface dotPreviewResourceLink { - key: string; - value: string; - show: boolean; -} - -@Component({ - selector: 'dot-binary-field-preview', - standalone: true, - imports: [ - CommonModule, - ButtonModule, - SkeletonModule, - DotTempFileThumbnailComponent, - DotSpinnerModule, - DialogModule, - DotMessagePipe, - DotFileSizeFormatPipe, - DotCopyButtonComponent - ], - providers: [DotResourceLinksService], - templateUrl: './dot-binary-field-preview.component.html', - styleUrls: ['./dot-binary-field-preview.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - schemas: [CUSTOM_ELEMENTS_SCHEMA] -}) -export class DotBinaryFieldPreviewComponent implements OnInit, OnChanges { - @Input() contentlet: DotCMSContentlet; - @Input() tempFile: DotCMSTempFile; - @Input() editableImage: boolean; - @Input() fieldVariable: string; - - @Output() editImage: EventEmitter = new EventEmitter(); - @Output() editFile: EventEmitter = new EventEmitter(); - @Output() removeFile: EventEmitter = new EventEmitter(); - - protected visibility = false; - protected isEditable = false; - protected readonly content = signal(''); - protected readonly resourceLinks = signal([]); - readonly #dotResourceLinksService = inject(DotResourceLinksService); - - get metadata(): DotFileMetadata { - return this.tempFile?.metadata ?? getFileMetadata(this.contentlet); - } - - get title(): string { - return this.contentlet?.fileName || this.metadata.name; - } - - get downloadLink(): string { - return `/contentAsset/raw-data/${this.contentlet.inode}/${this.fieldVariable}?byInode=true&force_download=true`; - } - - ngOnInit() { - if (this.contentlet) { - this.content.set(this.contentlet?.content); - this.fetchResourceLinks(); - } - } - - ngOnChanges({ tempFile, editableImage }: SimpleChanges): void { - if (editableImage) { - this.isEditable = this.isFileEditable(); - } - - if (tempFile?.currentValue) { - this.content.set(tempFile.currentValue.content); - } - } - - /** - * Emits event to remove the file - * - * @return {*} {void} - * @memberof DotBinaryFieldPreviewComponent - */ - onEdit(): void { - if (this.metadata.editableAsText) { - this.editFile.emit(); - - return; - } - - this.editImage.emit(); - } - - /** - * fetch the source links for the file - * - * @private - * @memberof DotBinaryFieldPreviewComponent - */ - private fetchResourceLinks(): void { - this.#dotResourceLinksService - .getFileResourceLinks({ - fieldVariable: this.fieldVariable, - inodeOrIdentifier: this.contentlet.identifier - }) - .pipe( - catchError(() => { - return of({ - configuredImageURL: '', - text: '', - versionPath: '', - idPath: '' - }); - }) - ) - .subscribe(({ configuredImageURL, text, versionPath, idPath }) => { - const fileLink = configuredImageURL - ? `${window.location.origin}${configuredImageURL}` - : ''; - - this.resourceLinks.set([ - { - key: 'FileLink', - value: fileLink, - show: true - }, - { - key: 'Resource-Link', - value: text, - show: this.contentlet.baseType === DotCMSBaseTypesContentTypes.FILEASSET - }, - { - key: 'VersionPath', - value: versionPath, - show: true - }, - { - key: 'IdPath', - value: idPath, - show: true - } - ]); - }); - } - - /** - * Emits event to remove the file - * - * @memberof DotBinaryFieldPreviewComponent - */ - downloadAsset(): void { - window.open(this.downloadLink, '_self'); - } - - /** - * Check if the file is editable - * - * @return {*} {boolean} - * @memberof DotBinaryFieldPreviewComponent - */ - private isFileEditable(): boolean { - return this.metadata.editableAsText || this.isEditableImage(); - } - - /** - * Check if the file is an editable image - * - * @private - * @return {*} {boolean} - * @memberof DotBinaryFieldPreviewComponent - */ - private isEditableImage(): boolean { - return this.metadata.isImage && this.editableImage; - } -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-ui-message/dot-binary-field-ui-message.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-ui-message/dot-binary-field-ui-message.component.html deleted file mode 100644 index c0042a5d8442..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-ui-message/dot-binary-field-ui-message.component.html +++ /dev/null @@ -1,7 +0,0 @@ -
- -
-
- - -
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-ui-message/dot-binary-field-ui-message.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-ui-message/dot-binary-field-ui-message.component.scss deleted file mode 100644 index fccd0d7f5e15..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-ui-message/dot-binary-field-ui-message.component.scss +++ /dev/null @@ -1,36 +0,0 @@ -@use "variables" as *; - -:host { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: $spacing-3; - height: 100%; - padding: $spacing-3; -} - -.icon-container { - border-radius: 50%; - padding: $spacing-3; - - .icon { - font-size: $font-size-xxl; - width: auto; - } - - &.info { - color: $color-palette-primary-500; - background: $color-palette-primary-200; - } - - &.error { - color: $color-alert-yellow; - background: $color-alert-yellow-light; - } -} - -.text { - text-align: center; - line-height: 140%; -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-ui-message/dot-binary-field-ui-message.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-ui-message/dot-binary-field-ui-message.component.spec.ts deleted file mode 100644 index 43e05acf548d..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-ui-message/dot-binary-field-ui-message.component.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { byTestId, createHostFactory, SpectatorHost } from '@ngneat/spectator'; -import { mockProvider } from '@ngneat/spectator/jest'; -import { MockComponent } from 'ng-mocks'; - -import { CommonModule } from '@angular/common'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; - -import { DotMessagePipe } from '@dotcms/ui'; - -import { DotBinaryFieldUiMessageComponent } from './dot-binary-field-ui-message.component'; - -import { DotBinaryFieldEditorComponent } from '../dot-binary-field-editor/dot-binary-field-editor.component'; - -describe('DotBinaryFieldUiMessageComponent', () => { - let spectator: SpectatorHost; - - const createHost = createHostFactory({ - component: DotBinaryFieldUiMessageComponent, - imports: [ - CommonModule, - DotMessagePipe, - HttpClientTestingModule, - MockComponent(DotBinaryFieldEditorComponent) - ], - providers: [mockProvider(DotMessagePipe)] - }); - - beforeEach(async () => { - spectator = createHost( - ` - - `, - { - hostProps: { - uiMessage: { - message: 'Drag and Drop File', - icon: 'pi pi-upload', - severity: 'info' - } - } - } - ); - spectator.detectChanges(); - await spectator.fixture.whenStable(); - }); - - it('should have a message, icon, and serverity', () => { - const messageText = spectator.query(byTestId('ui-message-span')).innerHTML; - const messageIconClass = spectator.query(byTestId('ui-message-icon')).className; - const messageIconContainer = spectator.query( - byTestId('ui-message-icon-container') - ).className; - - expect(messageText).toBe('Drag and Drop File'); - expect(messageIconClass).toBe('icon pi pi-upload'); - expect(messageIconContainer).toBe('icon-container info'); - }); - - it('should have a button', () => { - const button = spectator.query(byTestId('choose-file-btn')); - expect(button).toBeTruthy(); - }); -}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-ui-message/dot-binary-field-ui-message.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-ui-message/dot-binary-field-ui-message.component.ts deleted file mode 100644 index 21279aaf5f76..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-ui-message/dot-binary-field-ui-message.component.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; - -import { DotMessagePipe } from '@dotcms/ui'; - -import { UiMessageI } from '../../interfaces'; - -@Component({ - selector: 'dot-binary-field-ui-message', - standalone: true, - imports: [CommonModule, DotMessagePipe], - templateUrl: './dot-binary-field-ui-message.component.html', - styleUrls: ['./dot-binary-field-ui-message.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class DotBinaryFieldUiMessageComponent { - @Input() uiMessage: UiMessageI; -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/dot-binary-field-url-mode.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/dot-binary-field-url-mode.component.html deleted file mode 100644 index ad02afb91d1b..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/dot-binary-field-url-mode.component.html +++ /dev/null @@ -1,54 +0,0 @@ -@if (vm$ | async; as vm) { -
-
- - -
- @if (!vm.error) { - - } @else { - - {{ vm.error | dm: [acceptTypes] }} - - } -
-
-
- -
- @if (!vm.isLoading) { - - } @else { - - } -
-
-
-} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/dot-binary-field-url-mode.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/dot-binary-field-url-mode.component.scss deleted file mode 100644 index b82643dbfdc7..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/dot-binary-field-url-mode.component.scss +++ /dev/null @@ -1,37 +0,0 @@ -@use "variables" as *; - -:host ::ng-deep { - display: block; - width: 32rem; - - .p-button { - width: 100%; - } - - .error-messsage__container { - min-height: $spacing-4; // Fix height to avoid jumping - } -} - -.url-mode__form { - display: flex; - flex-direction: column; - gap: $spacing-3; - justify-content: center; - align-items: flex-start; -} - -.url-mode__input-container { - width: 100%; - display: flex; - gap: $spacing-1; - flex-direction: column; -} - -.url-mode__actions { - width: 100%; - display: flex; - gap: $spacing-1; - align-items: center; - justify-content: flex-end; -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/dot-binary-field-url-mode.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/dot-binary-field-url-mode.component.spec.ts deleted file mode 100644 index 504fed2f596d..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/dot-binary-field-url-mode.component.spec.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { expect, it } from '@jest/globals'; -import { Spectator, byTestId, createComponentFactory } from '@ngneat/spectator'; - -import { By } from '@angular/platform-browser'; - -import { ButtonModule } from 'primeng/button'; - -import { DotMessageService, DotUploadService } from '@dotcms/data-access'; - -import { DotBinaryFieldUrlModeComponent } from './dot-binary-field-url-mode.component'; -import { DotBinaryFieldUrlModeStore } from './store/dot-binary-field-url-mode.store'; - -import { DotBinaryFieldValidatorService } from '../../service/dot-binary-field-validator/dot-binary-field-validator.service'; -import { TEMP_FILE_MOCK } from '../../store/file-field.store.spec'; -import { CONTENTTYPE_FIELDS_MESSAGE_MOCK } from '../../utils/mock'; - -describe('DotBinaryFieldUrlModeComponent', () => { - let spectator: Spectator; - let component: DotBinaryFieldUrlModeComponent; - - let store: DotBinaryFieldUrlModeStore; - - const createComponent = createComponentFactory({ - component: DotBinaryFieldUrlModeComponent, - imports: [ButtonModule], - componentProviders: [DotBinaryFieldUrlModeStore], - providers: [ - DotBinaryFieldValidatorService, - { - provide: DotUploadService, - useValue: { - uploadFile: ({ file }) => { - return new Promise((resolve) => { - if (file) { - resolve(TEMP_FILE_MOCK); - } - }); - } - } - }, - { - provide: DotMessageService, - useValue: CONTENTTYPE_FIELDS_MESSAGE_MOCK - } - ] - }); - - beforeEach(() => { - spectator = createComponent({ - detectChanges: false - }); - - component = spectator.component; - store = spectator.inject(DotBinaryFieldUrlModeStore, true); - spectator.detectChanges(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should have a form with url field', () => { - expect(spectator.query(byTestId('url-input'))).not.toBeNull(); - }); - - it('should have a button to import', () => { - expect(spectator.query(byTestId('import-button'))).not.toBeNull(); - }); - - describe('Actions', () => { - it('should upload file by url form when click on import button', async () => { - const spy = jest.spyOn(component.tempFileUploaded, 'emit'); - const spyUploadFileByUrl = jest.spyOn(store, 'uploadFileByUrl'); - const importButton = spectator.query('[data-testId="import-button"] button'); - const form = spectator.component.form; - - form.setValue({ url: 'http://dotcms.com' }); - spectator.click(importButton); - - expect(spy).toHaveBeenCalledWith(TEMP_FILE_MOCK); - expect(spectator.component.form.valid).toBeTruthy(); - expect(spyUploadFileByUrl).toHaveBeenCalled(); - }); - - it('should cancel when click on cancel button', () => { - const spyCancel = jest.spyOn(spectator.component.cancel, 'emit'); - const cancelButton = spectator.query('[data-testId="cancel-button"] button'); - - spectator.click(cancelButton); - - expect(spyCancel).toHaveBeenCalled(); - }); - - it('should show loading button when isLoading', async () => { - store.setIsLoading(true); - await spectator.fixture.whenStable(); - spectator.detectChanges(); - - const loadingButton = spectator.query(byTestId('loading-button')); - const importButton = spectator.query(byTestId('import-button')); - - expect(loadingButton).toBeTruthy(); - expect(importButton).not.toBeTruthy(); - }); - }); - - describe('validation', () => { - it('should be invalid when url is empty', () => { - spectator.component.form.setValue({ url: '' }); - expect(spectator.component.form.valid).toBe(false); - }); - - it('should be invalid when url is not valid', () => { - spectator.component.form.setValue({ url: 'Not a url' }); - expect(spectator.component.form.valid).toBe(false); - }); - - it('should be valid when url is valid', () => { - spectator.component.form.setValue({ url: 'http://dotcms.com' }); - expect(spectator.component.form.valid).toBeTruthy(); - }); - - it('should show error when value is empty and user is trying to upload file ', async () => { - const button = spectator.query('[data-testId="import-button"] button'); - const form = spectator.component.form; - - form.setValue({ url: '' }); - spectator.click(button); - spectator.detectChanges(); - await spectator.fixture.whenStable(); - - const fieldMessage = spectator.fixture.debugElement.query( - By.css('dot-field-validation-message') - ); - const error = fieldMessage.componentInstance.defaultMessage; - - expect(spectator.component.form.invalid).toBeTruthy(); - expect(error).toBe('The URL you requested is not valid. Please try again.'); - }); - }); - - describe('template', () => { - it('should show error message when url is invalid', () => { - const input = spectator.query(byTestId('url-input')); - - input.focus(); // to trigger touched - input.value = 'Not a url'; // to trigger invalid - input.blur(); // to trigger dirty - - expect(spectator.query(byTestId('error-message'))).toBeTruthy(); - }); - }); -}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/dot-binary-field-url-mode.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/dot-binary-field-url-mode.component.ts deleted file mode 100644 index be2f2cecb424..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/dot-binary-field-url-mode.component.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Subject } from 'rxjs'; - -import { AsyncPipe } from '@angular/common'; -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - OnDestroy, - OnInit, - Output, - inject -} from '@angular/core'; -import { - FormGroup, - FormControl, - FormsModule, - ReactiveFormsModule, - Validators -} from '@angular/forms'; - -import { ButtonModule } from 'primeng/button'; -import { InputTextModule } from 'primeng/inputtext'; - -import { filter, takeUntil, tap } from 'rxjs/operators'; - -import { DotCMSTempFile } from '@dotcms/dotcms-models'; -import { DotFieldValidationMessageComponent, DotMessagePipe } from '@dotcms/ui'; - -import { DotBinaryFieldUrlModeStore } from './store/dot-binary-field-url-mode.store'; - -import { DotBinaryFieldValidatorService } from '../../service/dot-binary-field-validator/dot-binary-field-validator.service'; - -@Component({ - selector: 'dot-binary-field-url-mode', - standalone: true, - imports: [ - FormsModule, - ReactiveFormsModule, - ButtonModule, - InputTextModule, - DotMessagePipe, - DotFieldValidationMessageComponent, - AsyncPipe - ], - providers: [DotBinaryFieldUrlModeStore], - templateUrl: './dot-binary-field-url-mode.component.html', - styleUrls: ['./dot-binary-field-url-mode.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class DotBinaryFieldUrlModeComponent implements OnInit, OnDestroy { - @Output() tempFileUploaded: EventEmitter = new EventEmitter(); - @Output() cancel: EventEmitter = new EventEmitter(); - - private readonly store = inject(DotBinaryFieldUrlModeStore); - private readonly dotBinaryFieldValidatorService = inject(DotBinaryFieldValidatorService); - - // Form - private readonly validators = [ - Validators.required, - Validators.pattern(/^(ftp|http|https):\/\/[^ "]+$/) - ]; - readonly form = new FormGroup({ - url: new FormControl('', this.validators) - }); - - // Observables - readonly vm$ = this.store.vm$.pipe(tap(({ isLoading }) => this.toggleForm(isLoading))); - readonly tempFileChanged$ = this.store.tempFile$; - - private readonly destroy$ = new Subject(); - private abortController: AbortController; - - get acceptTypes(): string { - return this.dotBinaryFieldValidatorService.accept.join(','); - } - - ngOnInit(): void { - this.tempFileChanged$ - .pipe( - takeUntil(this.destroy$), - filter((tempFile) => tempFile !== null) - ) - .subscribe((tempFile) => { - this.tempFileUploaded.emit(tempFile); - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - this.abortController?.abort(); // Abort fetch request if component is destroyed - } - - /** - * Submit form - * - * @return {*} {void} - * @memberof DotBinaryFieldUrlModeComponent - */ - onSubmit(): void { - if (this.form.invalid) { - return; - } - - const url = this.form.get('url').value; - this.abortController = new AbortController(); - - this.store.uploadFileByUrl({ url, signal: this.abortController.signal }); - this.form.reset({ url }); // Reset form to initial state - } - - /** - * Cancel upload - * - * @memberof DotBinaryFieldUrlModeComponent - */ - cancelUpload(): void { - this.abortController?.abort(); - this.cancel.emit(); - } - - /** - * Handle focus event and clear server error message - * - * @memberof DotBinaryFieldUrlModeComponent - */ - handleFocus(): void { - this.store.setError(''); // Clear server error message when user focus on input - } - - private toggleForm(isLoading: boolean): void { - isLoading ? this.form.disable() : this.form.enable(); - } -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/store/dot-binary-field-url-mode.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/store/dot-binary-field-url-mode.spec.ts deleted file mode 100644 index edb0c7398876..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/store/dot-binary-field-url-mode.spec.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { expect, describe, jest } from '@jest/globals'; -import { SpectatorService, createServiceFactory } from '@ngneat/spectator'; - -import { skip } from 'rxjs/operators'; - -import { DotUploadService } from '@dotcms/data-access'; -import { DotCMSTempFile } from '@dotcms/dotcms-models'; - -import { - DotBinaryFieldUrlModeState, - DotBinaryFieldUrlModeStore -} from './dot-binary-field-url-mode.store'; - -import { DotBinaryFieldValidatorService } from '../../../service/dot-binary-field-validator/dot-binary-field-validator.service'; - -const INITIAL_STATE: DotBinaryFieldUrlModeState = { - tempFile: null, - isLoading: false, - error: '' -}; - -export const TEMP_FILE_MOCK: DotCMSTempFile = { - fileName: 'image.png', - folder: '/images', - id: '12345', - image: true, - length: 1000, - referenceUrl: '/reference/url', - thumbnailUrl: 'image.png', - mimeType: 'mimeType' -}; - -describe('DotBinaryFieldUrlModeStore', () => { - let spectator: SpectatorService; - let store: DotBinaryFieldUrlModeStore; - let dotBinaryFieldValidatorService: DotBinaryFieldValidatorService; - - let dotUploadService: DotUploadService; - let initialState; - - const createStoreService = createServiceFactory({ - service: DotBinaryFieldUrlModeStore, - providers: [ - DotBinaryFieldValidatorService, - { - provide: DotUploadService, - useValue: { - uploadFile: ({ file }) => { - return new Promise((resolve) => { - if (file) { - resolve(TEMP_FILE_MOCK); - } - }); - } - } - } - ] - }); - - beforeEach(() => { - spectator = createStoreService(); - dotUploadService = spectator.inject(DotUploadService); - store = spectator.inject(DotBinaryFieldUrlModeStore); - dotBinaryFieldValidatorService = spectator.inject(DotBinaryFieldValidatorService); - dotBinaryFieldValidatorService.setMaxFileSize(1048576); - - store.setState(INITIAL_STATE); - store.state$.subscribe((state) => { - initialState = state; - }); - }); - - it('should set initial state', () => { - expect(initialState).toEqual(INITIAL_STATE); - }); - - describe('Updaters', () => { - it('should set TempFile', (done) => { - store.setTempFile(TEMP_FILE_MOCK); - - store.tempFile$.subscribe((tempFile) => { - expect(tempFile).toEqual(TEMP_FILE_MOCK); - done(); - }); - }); - - it('should set isLoading', (done) => { - store.setIsLoading(true); - - store.vm$.subscribe((state) => { - expect(state.isLoading).toBeTruthy(); - done(); - }); - }); - - it('should set error and isLoading to false', (done) => { - store.setIsLoading(true); // Set isLoading to true - store.setError('Request Error'); // Set error and isLoading to false - - // Skip setIsLoading - store.vm$.subscribe((state) => { - expect(state.error).toBe('Request Error'); - expect(state.isLoading).toBeFalsy(); - done(); - }); - }); - }); - - describe('Actions', () => { - describe('handleUploadFile', () => { - it('should set tempFile and loading to false', (done) => { - const spySetIsLoading = jest.spyOn(store, 'setIsLoading'); - const abortController = new AbortController(); - - store.uploadFileByUrl({ - url: 'url', - signal: abortController.signal - }); - - // Skip initial state - store.tempFile$.pipe(skip(1)).subscribe((tempFile) => { - expect(tempFile).toEqual(TEMP_FILE_MOCK); - done(); - }); - - expect(spySetIsLoading).toHaveBeenCalledWith(true); - }); - - it('should called tempFile API with 1MB', (done) => { - const spyOnUploadService = jest.spyOn(dotUploadService, 'uploadFile'); - - // 1MB - store.setMaxFileSize(1048576); - const abortController = new AbortController(); - - store.uploadFileByUrl({ - url: 'url', - signal: abortController.signal - }); - - // Skip initial state - store.tempFile$.pipe(skip(1)).subscribe(() => { - expect(spyOnUploadService).toHaveBeenCalledWith({ - file: 'url', - maxSize: '1MB', - signal: abortController.signal - }); - done(); - }); - }); - }); - }); -}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/store/dot-binary-field-url-mode.store.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/store/dot-binary-field-url-mode.store.ts deleted file mode 100644 index 91348cc15a9a..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-binary-field-url-mode/store/dot-binary-field-url-mode.store.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { Observable, from } from 'rxjs'; - -import { Injectable } from '@angular/core'; - -import { switchMap, tap } from 'rxjs/operators'; - -import { DotUploadService } from '@dotcms/data-access'; -import { DotCMSTempFile, DotHttpErrorResponse } from '@dotcms/dotcms-models'; - -import { DotBinaryFieldValidatorService } from '../../../service/dot-binary-field-validator/dot-binary-field-validator.service'; - -export interface DotBinaryFieldUrlModeState { - tempFile: DotCMSTempFile; - isLoading: boolean; - error: string; -} - -@Injectable() -export class DotBinaryFieldUrlModeStore extends ComponentStore { - private _maxFileSize: number; - private _accept: string[]; - - readonly vm$ = this.select((state) => state); - - readonly tempFile$ = this.select(({ tempFile }) => tempFile); - - readonly error$ = this.select(({ error }) => error); - - constructor( - private readonly dotUploadService: DotUploadService, - private readonly dotBinaryFieldValidatorService: DotBinaryFieldValidatorService - ) { - super({ - tempFile: null, - isLoading: false, - error: '' - }); - - this.setMaxFileSize(this.dotBinaryFieldValidatorService.maxFileSize); - } - - // Update state - readonly setTempFile = this.updater((state, tempFile: DotCMSTempFile) => { - return { - ...state, - tempFile, - isLoading: false, - error: '' - }; - }); - - readonly setIsLoading = this.updater((state, isLoading: boolean) => { - return { - ...state, - isLoading - }; - }); - - readonly setError = this.updater((state, error: string) => { - return { - ...state, - isLoading: false, - error - }; - }); - - // Actions - readonly uploadFileByUrl = this.effect( - ( - data$: Observable<{ - url: string; - signal: AbortSignal; - }> - ) => { - return data$.pipe( - tap(() => this.setIsLoading(true)), - switchMap(({ url, signal }) => this.uploadTempFile(url, signal)) - ); - } - ); - - private uploadTempFile(file: File | string, signal: AbortSignal): Observable { - return from( - this.dotUploadService.uploadFile({ - file, - maxSize: this._maxFileSize ? `${this._maxFileSize}MB` : '', - signal: signal - }) - ).pipe( - tapResponse( - (tempFile: DotCMSTempFile) => { - if (!this.isValidType(tempFile)) { - this.setError( - 'dot.binary.field.import.from.url.error.file.not.supported.message' - ); - - return; - } - - this.setTempFile(tempFile); - }, - (error: DotHttpErrorResponse) => { - if (signal.aborted) { - this.setIsLoading(false); - - return; - } - - this.setError(error.message); - } - ) - ); - } - - setMaxFileSize(bytes: number) { - this._maxFileSize = this._maxFileSize = bytes / (1024 * 1024); - } - - /** - * Validate file type - * - * @private - * @return {*} {boolean} - * @memberof DotBinaryFieldUrlModeStore - */ - private isValidType(tempFile: DotCMSTempFile): boolean { - const { fileName, mimeType } = tempFile; - const extension = fileName.split('.').pop(); - - return this.dotBinaryFieldValidatorService.isValidType({ - extension, - mimeType - }); - } -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.html index 3d50f8d69ac6..8be12d24bae5 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.html +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.html @@ -1,133 +1,2 @@ -@if (vm$ | async; as vm) { - -
- @switch (vm.status) { - @case (BinaryFieldStatus.INIT) { -
- - - - - - -
- -
- @if (systemOptions().allowURLImport) { - - } - @if (systemOptions().allowCodeWrite) { - - } - @if (systemOptions().allowGenerateImg) { - - - - - - - - {{ 'dot.binary.field.action.generate.with.dotai' | dm }} - - - } -
- } - @case (BinaryFieldStatus.UPLOADING) { - - } - @case (BinaryFieldStatus.PREVIEW) { - - } - } - - - @switch (vm.mode) { - @case (BinaryFieldMode.URL) { - - } - @case (BinaryFieldMode.EDITOR) { - - } - } - -
-} +

Hello

+

{{ $value() }}

\ No newline at end of file diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.spec.ts deleted file mode 100644 index faef9c7f29c8..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.spec.ts +++ /dev/null @@ -1,604 +0,0 @@ -import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'; -import { - byTestId, - createComponentFactory, - createHostFactory, - Spectator, - SpectatorHost, - SpyObject -} from '@ngneat/spectator/jest'; -import { of } from 'rxjs'; - -import { Component, NgZone } from '@angular/core'; -import { fakeAsync, tick } from '@angular/core/testing'; -import { - ControlContainer, - FormControl, - FormGroup, - FormGroupDirective, - ReactiveFormsModule -} from '@angular/forms'; -import { By } from '@angular/platform-browser'; - -import { ButtonModule, Button } from 'primeng/button'; -import { DialogModule } from 'primeng/dialog'; - -import { - DotAiService, - DotLicenseService, - DotMessageService, - DotUploadService -} from '@dotcms/data-access'; -import { DotCMSTempFile } from '@dotcms/dotcms-models'; -import { DotEditContentBinaryFieldComponent } from '@dotcms/edit-content'; -import { DotAiImagePromptStore, DropZoneErrorType, DropZoneFileEvent } from '@dotcms/ui'; -import { dotcmsContentletMock } from '@dotcms/utils-testing'; - -import { DotBinaryFieldPreviewComponent } from './components/dot-binary-field-preview/dot-binary-field-preview.component'; -import { BinaryFieldMode, BinaryFieldStatus } from './interfaces'; -import { DotBinaryFieldEditImageService } from './service/dot-binary-field-edit-image/dot-binary-field-edit-image.service'; -import { DotBinaryFieldValidatorService } from './service/dot-binary-field-validator/dot-binary-field-validator.service'; -import { DotBinaryFieldStore } from './store/file-field.store'; -import { getUiMessage } from './utils/binary-field-utils'; -import { CONTENTTYPE_FIELDS_MESSAGE_MOCK, fileMetaData } from './utils/mock'; - -import { BINARY_FIELD_MOCK, createFormGroupDirectiveMock } from '../../utils/mocks'; - -const TEMP_FILE_MOCK: DotCMSTempFile = { - fileName: 'image.png', - folder: '/images', - id: '123456', - image: true, - length: 1000, - referenceUrl: - 'https://images.unsplash.com/photo-1575936123452-b67c3203c357?auto=format&fit=crop&q=80&w=1000&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8aW1hZ2V8ZW58MHx8MHx8fDA%3D', - thumbnailUrl: 'image.png', - mimeType: 'mimeType', - metadata: fileMetaData -}; - -const file = new File([''], 'filename'); -const validity = { - valid: true, - fileTypeMismatch: false, - maxFileSizeExceeded: false, - multipleFilesDropped: false, - errorsType: [DropZoneErrorType.FILE_TYPE_MISMATCH] -}; - -const DROP_ZONE_FILE_EVENT: DropZoneFileEvent = { - file, - validity -}; - -const MOCK_DOTCMS_FILE = { - ...dotcmsContentletMock, - binaryField: '12345', - baseType: 'CONTENT', - binaryFieldMetaData: fileMetaData -}; - -describe('DotEditContentBinaryFieldComponent', () => { - let spectator: Spectator; - let store: DotBinaryFieldStore; - - let dotBinaryFieldEditImageService: SpyObject; - let dotAiService: DotAiService; - let ngZone: NgZone; - - const createComponent = createComponentFactory({ - component: DotEditContentBinaryFieldComponent, - componentProviders: [ - DotBinaryFieldStore, - DotAiImagePromptStore, - DotBinaryFieldEditImageService, - DotAiService - ], - componentViewProviders: [ - { provide: ControlContainer, useValue: createFormGroupDirectiveMock() } - ], - providers: [ - DotBinaryFieldValidatorService, - { - provide: DotLicenseService, - useValue: { - isEnterprise: () => of(true) - } - }, - { - provide: DotUploadService, - useValue: { - uploadFile: ({ file }) => { - return new Promise((resolve) => { - if (file) { - resolve(TEMP_FILE_MOCK); - } - }); - } - } - }, - { - provide: DotMessageService, - useValue: CONTENTTYPE_FIELDS_MESSAGE_MOCK - }, - FormGroupDirective - ] - }); - - beforeEach(() => { - spectator = createComponent({ - detectChanges: false, - props: { - field: { - ...BINARY_FIELD_MOCK - }, - contentlet: null - } - }); - store = spectator.inject(DotBinaryFieldStore, true); - dotBinaryFieldEditImageService = spectator.inject(DotBinaryFieldEditImageService, true); - dotAiService = spectator.inject(DotAiService, true); - ngZone = spectator.inject(NgZone); - }); - - it('shouldnt show url import button if not setted in settings', () => { - const importFromURLButton = spectator.query(byTestId('action-url-btn')); - - expect(importFromURLButton).toBeNull(); - }); - - it('shouldnt show code editor button if not setted in settings', async () => { - const codeEditorButton = spectator.query(byTestId('action-editor-btn')); - - expect(codeEditorButton).toBeNull(); - }); - - it('should emit temp file', () => { - const spyEmit = jest.spyOn(spectator.component.valueUpdated, 'emit'); - spectator.detectChanges(); - store.setTempFile(TEMP_FILE_MOCK); - expect(spyEmit).toHaveBeenCalledWith({ - value: TEMP_FILE_MOCK.id, - fileName: TEMP_FILE_MOCK.fileName - }); - }); - - it('should not emit new value is is equal to current value', () => { - spectator.setInput('contentlet', MOCK_DOTCMS_FILE); - const spyEmit = jest.spyOn(spectator.component.valueUpdated, 'emit'); - spectator.component.writeValue(MOCK_DOTCMS_FILE.binaryField); - store.setValue(MOCK_DOTCMS_FILE.binaryField); - spectator.detectChanges(); - expect(spyEmit).not.toHaveBeenCalled(); - }); - - describe('Dropzone', () => { - beforeEach(async () => { - spectator.setInput('contentlet', MOCK_DOTCMS_FILE); - spectator.detectChanges(); - store.setStatus(BinaryFieldStatus.INIT); - await spectator.fixture.whenStable(); - spectator.detectChanges(); - }); - - it('should show dropzone when status is INIT', () => { - expect(spectator.query('dot-drop-zone')).toBeTruthy(); - }); - - it('should handle file drop', () => { - const spyUploadFile = jest.spyOn(store, 'handleUploadFile'); - const spyInvalidFile = jest.spyOn(store, 'invalidFile'); - const dropZone = spectator.fixture.debugElement.query(By.css('dot-drop-zone')); - - dropZone.triggerEventHandler('fileDropped', DROP_ZONE_FILE_EVENT); - - expect(spyUploadFile).toHaveBeenCalledWith(DROP_ZONE_FILE_EVENT.file); - expect(spyInvalidFile).not.toHaveBeenCalled(); - }); - - it('should handle file drop error', () => { - const spyUploadFile = jest.spyOn(store, 'handleUploadFile'); - const spyInvalidFile = jest.spyOn(store, 'invalidFile'); - const dropZone = spectator.fixture.debugElement.query(By.css('dot-drop-zone')); - - dropZone.triggerEventHandler('fileDropped', { - ...DROP_ZONE_FILE_EVENT, - validity: { - ...DROP_ZONE_FILE_EVENT.validity, - fileTypeMismatch: true, - valid: false - } - }); - - expect(spyInvalidFile).toHaveBeenCalledWith( - getUiMessage('FILE_TYPE_MISMATCH', 'image/*, .html, .ts') - ); - expect(spyUploadFile).not.toHaveBeenCalled(); - }); - - it('should handle file dragover', () => { - const dropZone = spectator.fixture.debugElement.query(By.css('dot-drop-zone')); - const spyDropZoneActive = jest.spyOn(store, 'setDropZoneActive'); - dropZone.triggerEventHandler('fileDragOver', {}); - - expect(spyDropZoneActive).toHaveBeenCalledWith(true); - }); - - it('should handle file dragleave', () => { - const dropZone = spectator.fixture.debugElement.query(By.css('dot-drop-zone')); - const spyDropZoneActive = jest.spyOn(store, 'setDropZoneActive'); - dropZone.triggerEventHandler('fileDragLeave', {}); - - expect(spyDropZoneActive).toHaveBeenCalledWith(false); - }); - - it('should open file picker when click on choose file button', () => { - const spyOpenFilePicker = jest.spyOn(spectator.component, 'openFilePicker'); - const spyInputFile = jest.spyOn(spectator.component.inputFile.nativeElement, 'click'); - const chooseFile = spectator.query(byTestId('choose-file-btn')) as HTMLButtonElement; - chooseFile.click(); - expect(chooseFile.getAttribute('type')).toBe('button'); - expect(spyOpenFilePicker).toHaveBeenCalled(); - expect(spyInputFile).toHaveBeenCalled(); - }); - - it('should handle file selection', () => { - const spyUploadFile = jest.spyOn(store, 'handleUploadFile'); - const inputElement = spectator.fixture.debugElement.query( - By.css('[data-testId="binary-field__file-input"]') - ).nativeElement; - const file = new File(['test'], 'test.png', { type: 'image/png' }); - const event = new Event('change'); - Object.defineProperty(event, 'target', { value: { files: [file] } }); - inputElement.dispatchEvent(event); - - expect(spyUploadFile).toHaveBeenCalledWith(file); - }); - }); - - describe('Preview', () => { - beforeEach(async () => { - store.setStatus(BinaryFieldStatus.PREVIEW); - store.setTempFile(TEMP_FILE_MOCK); - spectator.detectChanges(); - await spectator.fixture.whenStable(); - }); - - it('should remove file and set INIT status when remove file ', async () => { - const spyRemoveFile = jest.spyOn(store, 'removeFile'); - const dotBinaryPreviewFile = spectator.fixture.debugElement.query( - By.css('[data-testId="preview"]') - ); - - dotBinaryPreviewFile.componentInstance.removeFile.emit(); - - store.vm$.subscribe((state) => { - expect(state).toEqual({ - ...state, - status: BinaryFieldStatus.INIT, - value: '', - contentlet: null, - tempFile: null - }); - }); - - spectator.detectChanges(); - await spectator.fixture.whenStable(); - - const dropZone = spectator.fixture.debugElement.query(By.css('dot-drop-zone')); - - expect(dropZone).toBeTruthy(); - expect(spyRemoveFile).toHaveBeenCalled(); - }); - - describe('Edit Image', () => { - it('should open edit image dialog when click on edit image button', () => { - spectator.detectChanges(); - const spy = jest.spyOn(dotBinaryFieldEditImageService, 'openImageEditor'); - spectator.triggerEventHandler(DotBinaryFieldPreviewComponent, 'editImage', null); - expect(spy).toHaveBeenCalled(); - }); - - it('should emit the tempId of the edited image', () => { - // Needed because the openImageEditor method is using a DOM custom event - ngZone.run( - fakeAsync(() => { - const spy = jest.spyOn(dotBinaryFieldEditImageService, 'openImageEditor'); - const spyTempFile = jest.spyOn(store, 'setFileFromTemp'); - const dotBinaryFieldPreviewComponent = spectator.fixture.debugElement.query( - By.css('dot-binary-field-preview') - ); - dotBinaryFieldPreviewComponent.triggerEventHandler('editImage'); - const customEvent = new CustomEvent( - `binaryField-tempfile-${BINARY_FIELD_MOCK.variable}`, - { - detail: { tempFile: TEMP_FILE_MOCK } - } - ); - document.dispatchEvent(customEvent); - - tick(1000); - - expect(spy).toHaveBeenCalled(); - expect(spyTempFile).toHaveBeenCalledWith(TEMP_FILE_MOCK); - }) - ); - }); - }); - }); - - describe('Template', () => { - beforeEach(() => { - spectator.detectChanges(); - }); - - it('should show dropzone when status is INIT', async () => { - store.setStatus(BinaryFieldStatus.INIT); - spectator.detectChanges(); - await spectator.fixture.whenStable(); - expect(spectator.query(byTestId('dropzone'))).toBeTruthy(); - }); - - it('should show loading when status is UPLOADING', async () => { - store.setStatus(BinaryFieldStatus.UPLOADING); - spectator.detectChanges(); - await spectator.fixture.whenStable(); - expect(spectator.query(byTestId('loading'))).toBeTruthy(); - }); - - it('should show preview when status is PREVIEW', async () => { - store.setTempFile(TEMP_FILE_MOCK); - spectator.detectChanges(); - - await spectator.fixture.whenStable(); - - expect(spectator.query(byTestId('preview'))).toBeTruthy(); - }); - }); - - describe('systemOptions all false', () => { - beforeEach(() => { - const systemOptions = { - allowURLImport: false, - allowCodeWrite: false, - allowGenerateImg: false - }; - - const JSONString = JSON.stringify(systemOptions); - - const newField = { - ...BINARY_FIELD_MOCK, - fieldVariables: [ - ...BINARY_FIELD_MOCK.fieldVariables, - { - clazz: 'com.dotcms.contenttype.model.field.ImmutableFieldVariable', - fieldId: '5df3f8fc49177c195740bcdc02ec2db7', - id: '1ff1ff05-b9fb-4239-ad3d-b2cfaa9a8406', - key: 'systemOptions', - value: JSONString - } - ] - }; - - spectator = createComponent({ - detectChanges: false, - props: { - field: newField, - contentlet: null - } - }); - }); - - it('should show url import button if not setted in settings', () => { - spectator.detectChanges(); - const importFromURLButton = spectator.query(byTestId('action-url-btn')); - - expect(importFromURLButton).toBeNull(); - }); - - it('should show code editor button if not setted in settings', async () => { - spectator.detectChanges(); - const codeEditorButton = spectator.query(byTestId('action-editor-btn')); - - expect(codeEditorButton).toBeNull(); - }); - - it('should show code ai button if not setted in settings', async () => { - spectator.detectChanges(); - const codeEditorButton = spectator.query(byTestId('action-ai-btn')); - - expect(codeEditorButton).toBeNull(); - }); - }); - - describe('Ai option', () => { - beforeEach(() => { - const systemOptions = { - allowURLImport: true, - allowCodeWrite: true, - allowGenerateImg: true - }; - - const JSONString = JSON.stringify(systemOptions); - - const newField = { - ...BINARY_FIELD_MOCK, - fieldVariables: [ - ...BINARY_FIELD_MOCK.fieldVariables, - { - clazz: 'com.dotcms.contenttype.model.field.ImmutableFieldVariable', - fieldId: '5df3f8fc49177c195740bcdc02ec2db7', - id: '1ff1ff05-b9fb-4239-ad3d-b2cfaa9a8406', - key: 'systemOptions', - value: JSONString - } - ] - }; - - spectator = createComponent({ - detectChanges: false, - props: { - field: newField, - contentlet: null - } - }); - }); - - it('should show ai button', async () => { - spectator.detectChanges(); - const codeEditorButton = spectator.query(byTestId('action-ai-btn')); - expect(codeEditorButton).toBeTruthy(); - }); - - it('should AI button is disabled when plugin is not installed', async () => { - dotAiService.checkPluginInstallation = jest.fn().mockReturnValue(of(false)); - spectator.detectChanges(); - const buttons = spectator.queryAll(Button); - const aiBtn = buttons[2]; - expect(aiBtn.disabled).toBe(true); - expect(aiBtn.styleClass).toContain('pointer-events-auto'); - }); - }); - - describe('Dialog', () => { - beforeEach(async () => { - jest.spyOn(store, 'setFileFromContentlet').mockReturnValue(of(null).subscribe()); - - spectator.detectChanges(); - await spectator.fixture.whenStable(); - spectator.detectChanges(); - }); - - it('should open dialog with code component when click on edit button', async () => { - const spySetMode = jest.spyOn(store, 'setMode'); - const editorBtn = spectator.query(byTestId('action-editor-btn')) as HTMLButtonElement; - editorBtn.click(); - - spectator.detectChanges(); - await spectator.fixture.whenStable(); - - const editorElement = document.querySelector('[data-testid="editor-mode"]'); // This element is added to the body by the dialog - const isDialogOpen = spectator.fixture.componentInstance.openDialog; - - expect(editorElement).toBeTruthy(); - expect(isDialogOpen).toBeTruthy(); - expect(spySetMode).toHaveBeenCalledWith(BinaryFieldMode.EDITOR); - }); - - it('should open dialog with url component component when click on url button', async () => { - const spySetMode = jest.spyOn(store, 'setMode'); - const urlBtn = spectator.query(byTestId('action-url-btn')) as HTMLButtonElement; - urlBtn.click(); - - spectator.detectChanges(); - await spectator.fixture.whenStable(); - - const urlElement = document.querySelector('[data-testid="url-mode"]'); // This element is added to the body by the dialog - const isDialogOpen = spectator.fixture.componentInstance.openDialog; - - expect(urlElement).toBeTruthy(); - expect(isDialogOpen).toBeTruthy(); - expect(spySetMode).toHaveBeenCalledWith(BinaryFieldMode.URL); - }); - }); - - describe('Set File', () => { - describe('Contentlet - BaseTyp FILEASSET', () => { - it('should set the correct file asset', () => { - const spy = jest - .spyOn(store, 'setFileFromContentlet') - .mockReturnValue(of(null).subscribe()); - const mock = { - ...MOCK_DOTCMS_FILE, - baseType: 'FILEASSET', - metaData: fileMetaData - }; - spectator.setInput('contentlet', mock); - spectator.detectChanges(); - expect(spy).toHaveBeenCalledWith({ - ...mock, - fieldVariable: BINARY_FIELD_MOCK.variable, - value: mock[BINARY_FIELD_MOCK.variable] - }); - }); - }); - - describe('Contentlet - BaseTyp CONTENT', () => { - it('should set the correct file asset', () => { - const spy = jest - .spyOn(store, 'setFileFromContentlet') - .mockReturnValue(of(null).subscribe()); - const variable = BINARY_FIELD_MOCK.variable; - spectator.setInput('contentlet', MOCK_DOTCMS_FILE); - spectator.detectChanges(); - expect(spy).toHaveBeenCalledWith({ - ...MOCK_DOTCMS_FILE, - fieldVariable: variable, - value: MOCK_DOTCMS_FILE[variable] - }); - }); - }); - - it('should not set file when metadata is not present', () => { - const spy = jest - .spyOn(store, 'setFileFromContentlet') - .mockReturnValue(of(null).subscribe()); - const mock = { - ...MOCK_DOTCMS_FILE, - binaryFieldMetaData: null - }; - spectator.setInput('contentlet', mock); - spectator.detectChanges(); - expect(spy).not.toHaveBeenCalled(); - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); -}); - -/** - * - * @class MockFormComponent - */ -@Component({ - selector: 'dot-custom-host', - template: '' -}) -class MockFormComponent { - field = BINARY_FIELD_MOCK; - contentlet = MOCK_DOTCMS_FILE; - form = new FormGroup({ - binaryField: new FormControl('') - }); -} - -describe('DotEditContentBinaryFieldComponent - ControlValueAccessor', () => { - let spectator: SpectatorHost; - const createHost = createHostFactory({ - component: DotEditContentBinaryFieldComponent, - host: MockFormComponent, - imports: [ - ButtonModule, - DialogModule, - MonacoEditorModule, - ReactiveFormsModule, - DotEditContentBinaryFieldComponent - ], - providers: [DotAiService, DotAiImagePromptStore] - }); - - beforeEach(() => { - spectator = createHost(`
- -
`); - }); - - it('should set form value when binary file changes', () => { - // Call the onChange method from ControlValueAccessor - spectator.component.setTempFile(TEMP_FILE_MOCK); - const formValue = spectator.hostComponent.form.get('binaryField').value; // Get the form value - expect(formValue).toBe(TEMP_FILE_MOCK.id); // Check if the form value was set - }); -}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.stories.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.stories.ts deleted file mode 100644 index 7ee6b62f4dfb..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.stories.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'; -import { moduleMetadata, StoryObj, Meta } from '@storybook/angular'; -import { of } from 'rxjs'; - -import { CommonModule } from '@angular/common'; -import { HttpClientModule } from '@angular/common/http'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - -import { ButtonModule } from 'primeng/button'; -import { DialogModule } from 'primeng/dialog'; -import { InputTextModule } from 'primeng/inputtext'; - -import { DotLicenseService, DotMessageService, DotUploadService } from '@dotcms/data-access'; -import { - DotTempFileThumbnailComponent, - DotDropZoneComponent, - DotFieldValidationMessageComponent, - DotMessagePipe, - DotSpinnerModule -} from '@dotcms/ui'; - -import { DotBinaryFieldPreviewComponent } from './components/dot-binary-field-preview/dot-binary-field-preview.component'; -import { DotBinaryFieldUiMessageComponent } from './components/dot-binary-field-ui-message/dot-binary-field-ui-message.component'; -import { DotBinaryFieldUrlModeComponent } from './components/dot-binary-field-url-mode/dot-binary-field-url-mode.component'; -import { DotEditContentBinaryFieldComponent } from './dot-edit-content-file-field.component'; -import { DotBinaryFieldStore } from './store/file-field.store'; -import { CONTENTLET, CONTENTTYPE_FIELDS_MESSAGE_MOCK, TEMP_FILES_MOCK } from './utils/mock'; - -import { BINARY_FIELD_MOCK } from '../../utils/mocks'; - -const meta: Meta = { - title: 'Library / Edit Content / Binary Field', - component: DotEditContentBinaryFieldComponent, - decorators: [ - moduleMetadata({ - imports: [ - HttpClientModule, - BrowserAnimationsModule, - CommonModule, - ButtonModule, - DialogModule, - MonacoEditorModule, - DotDropZoneComponent, - DotBinaryFieldUiMessageComponent, - DotMessagePipe, - DotSpinnerModule, - InputTextModule, - DotBinaryFieldUrlModeComponent, - DotBinaryFieldPreviewComponent, - DotFieldValidationMessageComponent, - DotTempFileThumbnailComponent - ], - providers: [ - DotBinaryFieldStore, - { - provide: DotLicenseService, - useValue: { - isEnterprise: () => of(true) - } - }, - { - provide: DotUploadService, - useValue: { - uploadFile: () => { - return new Promise((resolve, _reject) => { - setTimeout(() => { - const index = Math.floor(Math.random() * 3); - const TEMP_FILE = TEMP_FILES_MOCK[index]; - resolve(TEMP_FILE); // TEMP_FILES_MOCK is imported from utils/mock.ts - }, 2000); - }); - } - } - }, - { - provide: DotMessageService, - useValue: CONTENTTYPE_FIELDS_MESSAGE_MOCK - } - ] - }) - ], - args: { - contentlet: CONTENTLET, - field: BINARY_FIELD_MOCK - }, - argTypes: { - contentlet: { - defaultValue: CONTENTLET, - control: 'object', - description: 'Contentlet Object' - }, - field: { - defaultValue: BINARY_FIELD_MOCK, - control: 'object', - description: 'Content Type Field Object' - } - }, - render: (args) => ({ - props: args, - template: `` - }) -}; -export default meta; - -type Story = StoryObj; - -export const Primary: Story = {}; diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.ts index ffd154e600d7..c0748915f2ae 100644 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.ts +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.component.ts @@ -1,107 +1,13 @@ -import { MonacoEditorConstructionOptions, MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'; +import { ChangeDetectionStrategy, Component, forwardRef, input, signal } from "@angular/core"; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; -import { CommonModule } from '@angular/common'; -import { HttpClientModule } from '@angular/common/http'; -import { - AfterViewInit, - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - computed, - effect, - ElementRef, - EventEmitter, - forwardRef, - inject, - Input, - OnDestroy, - OnInit, - Output, - signal, - Signal, - ViewChild -} from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; - -import { ButtonModule } from 'primeng/button'; -import { DialogModule } from 'primeng/dialog'; -import { InputTextModule } from 'primeng/inputtext'; -import { TooltipModule } from 'primeng/tooltip'; - -import { delay, filter, skip, tap } from 'rxjs/operators'; - -import { DotAiService, DotLicenseService, DotMessageService } from '@dotcms/data-access'; -import { - DotCMSBaseTypesContentTypes, - DotCMSContentlet, - DotCMSContentTypeField, - DotCMSContentTypeFieldVariable, - DotCMSTempFile, - DotGeneratedAIImage -} from '@dotcms/dotcms-models'; -import { - DotDropZoneComponent, - DotMessagePipe, - DotSpinnerModule, - DropZoneErrorType, - DropZoneFileEvent, - DropZoneFileValidity, - DotAIImagePromptComponent, - DotAiImagePromptStore -} from '@dotcms/ui'; - -import { DotBinaryFieldEditorComponent } from './components/dot-binary-field-editor/dot-binary-field-editor.component'; -import { DotBinaryFieldPreviewComponent } from './components/dot-binary-field-preview/dot-binary-field-preview.component'; -import { DotBinaryFieldUiMessageComponent } from './components/dot-binary-field-ui-message/dot-binary-field-ui-message.component'; -import { DotBinaryFieldUrlModeComponent } from './components/dot-binary-field-url-mode/dot-binary-field-url-mode.component'; -import { BinaryFieldMode, BinaryFieldStatus } from './interfaces'; -import { DotBinaryFieldEditImageService } from './service/dot-binary-field-edit-image/dot-binary-field-edit-image.service'; -import { DotBinaryFieldValidatorService } from './service/dot-binary-field-validator/dot-binary-field-validator.service'; -import { DotBinaryFieldStore } from './store/file-field.store'; -import { getUiMessage } from './utils/binary-field-utils'; - -import { DEFAULT_MONACO_CONFIG } from '../../models/dot-edit-content-field.constant'; -import { getFieldVariablesParsed, stringToJson } from '../../utils/functions.util'; - -export const DEFAULT_BINARY_FIELD_MONACO_CONFIG: MonacoEditorConstructionOptions = { - ...DEFAULT_MONACO_CONFIG, - language: 'text' -}; - -type SystemOptionsType = { - allowURLImport: boolean; - allowCodeWrite: boolean; - allowGenerateImg: boolean; -}; +import { DotCMSContentTypeField } from "@dotcms/dotcms-models"; @Component({ selector: 'dot-edit-content-file-field', standalone: true, - imports: [ - CommonModule, - ButtonModule, - DialogModule, - DotDropZoneComponent, - MonacoEditorModule, - DotMessagePipe, - DotBinaryFieldUiMessageComponent, - DotSpinnerModule, - HttpClientModule, - DotBinaryFieldEditorComponent, - InputTextModule, - DotBinaryFieldUrlModeComponent, - DotBinaryFieldPreviewComponent, - DotAIImagePromptComponent, - TooltipModule - ], + imports: [], providers: [ - DotBinaryFieldEditImageService, - DotBinaryFieldStore, - DotLicenseService, - DotBinaryFieldValidatorService, - DotAiImagePromptStore, - DotAiService, { multi: true, provide: NG_VALUE_ACCESSOR, @@ -112,166 +18,18 @@ type SystemOptionsType = { styleUrls: ['./dot-edit-content-file-field.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class DotEditContentFileFieldComponent - implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor -{ - readonly #dotBinaryFieldStore = inject(DotBinaryFieldStore); - readonly #dotAiImageStore = inject(DotAiImagePromptStore); - readonly #dotMessageService = inject(DotMessageService); - readonly #dotBinaryFieldEditImageService = inject(DotBinaryFieldEditImageService); - readonly #dotBinaryFieldValidatorService = inject(DotBinaryFieldValidatorService); - readonly #cd = inject(ChangeDetectorRef); - readonly #dotAiService = inject(DotAiService); - - $isAIPluginInstalled = toSignal(this.#dotAiService.checkPluginInstallation(), { - initialValue: false - }); - $tooltipTextAIBtn = computed(() => { - const isAIPluginInstalled = this.$isAIPluginInstalled(); - if (!isAIPluginInstalled) { - return this.#dotMessageService.get('dot.binary.field.action.generate.with.tooltip'); - } - - return null; - }); +export class DotEditContentFileFieldComponent implements ControlValueAccessor { - value: string | null = null; + $field = input.required({ alias: 'field' }); - @Input({ required: true }) - set field(contentTypeField: DotCMSContentTypeField) { - this.$field.set(contentTypeField); - } - @Input({ required: true }) contentlet: DotCMSContentlet; - @Input() imageEditor = false; - - $field = signal({} as DotCMSContentTypeField); - $variable = computed(() => this.$field()?.variable); - - @Output() valueUpdated = new EventEmitter<{ value: string; fileName: string }>(); - @ViewChild('inputFile') inputFile: ElementRef; - readonly dialogFullScreenStyles = { height: '90%', width: '90%' }; - readonly dialogHeaderMap = { - [BinaryFieldMode.URL]: 'dot.binary.field.dialog.import.from.url.header', - [BinaryFieldMode.EDITOR]: 'dot.binary.field.dialog.create.new.file.header' - }; - readonly BinaryFieldStatus = BinaryFieldStatus; - readonly BinaryFieldMode = BinaryFieldMode; - readonly vm$ = this.#dotBinaryFieldStore.vm$; - dialogOpen = false; - customMonacoOptions: Signal = computed(() => { - const field = this.$field(); - - return { - ...this.parseCustomMonacoOptions(field?.fieldVariables) - }; - }); private onChange: (value: string) => void; private onTouched: () => void; - private tempId = ''; - - systemOptions = signal({ - allowURLImport: false, - allowCodeWrite: false, - allowGenerateImg: false - }); - - isOpenDialog$ = this.#dotAiImageStore.isOpenDialog$; - $isOpenDialog = toSignal(this.isOpenDialog$); - selectedImage$ = this.#dotAiImageStore.selectedImage$; - $selectedImage = toSignal(this.selectedImage$); - - constructor() { - this.#dotMessageService.init(); - - effect( - () => { - const isOpenDialog = this.$isOpenDialog(); - if (isOpenDialog === false) { - this.closeDialog(); - } - }, - { - allowSignalWrites: true - } - ); - - effect( - () => { - const selectedImage = this.$selectedImage(); - if (selectedImage) { - const tempFile = this.parseToTempFile(selectedImage); - this.#dotAiImageStore.hideDialog(); - this.#dotBinaryFieldStore.setTempFile(tempFile); - } - }, - { - allowSignalWrites: true - } - ); - } - - get maxFileSize(): number { - return this.#dotBinaryFieldValidatorService.maxFileSize; - } - - get accept(): string[] { - return this.#dotBinaryFieldValidatorService.accept; - } - - get variable() { - return this.$variable(); - } - - ngOnInit() { - this.#dotBinaryFieldStore.value$ - .pipe( - skip(1), - filter(({ value }) => value !== this.getValue()) - ) - .subscribe(({ value, fileName }) => { - this.tempId = value; // If the value changes, it means that a new file was uploaded - this.valueUpdated.emit({ value, fileName }); - - if (this.onChange) { - this.onChange(value); - this.onTouched(); - } - }); - - this.#dotBinaryFieldEditImageService - .editedImage() - .pipe( - filter((tempFile) => !!tempFile), - tap(() => this.#dotBinaryFieldStore.setStatus(BinaryFieldStatus.UPLOADING)), - delay(500) // Loading animation - ) - .subscribe((temp) => this.#dotBinaryFieldStore.setFileFromTemp(temp)); - - this.#dotBinaryFieldStore.setMaxFileSize(this.maxFileSize); - } - - ngAfterViewInit() { - this.setFieldVariables(); - - if (!this.contentlet || !this.getValue() || !this.checkMetadata()) { - return; - } - - this.#dotBinaryFieldStore.setFileFromContentlet({ - ...this.contentlet, - value: this.getValue(), - fieldVariable: this.variable - }); - - this.#cd.detectChanges(); - } + $value = signal(''); writeValue(value: string): void { - this.value = value; - this.#dotBinaryFieldStore.setValue(value); + this.$value.set(value); } - registerOnChange(fn: (value: string) => void) { this.onChange = fn; } @@ -279,230 +37,5 @@ export class DotEditContentFileFieldComponent registerOnTouched(fn: () => void) { this.onTouched = fn; } - - ngOnDestroy() { - this.#dotBinaryFieldEditImageService.removeListener(); - } - - /** - * Open dialog to create new file or import from url - * - * @param {BinaryFieldMode} mode - * @memberof DotEditContentBinaryFieldComponent - */ - openDialog(mode: BinaryFieldMode) { - if (mode === BinaryFieldMode.AI) { - this.#dotAiImageStore.showDialog(''); - } else { - this.dialogOpen = true; - } - - this.#dotBinaryFieldStore.setMode(mode); - } - - /** - * Close dialog - * - * @memberof DotEditContentBinaryFieldComponent - */ - closeDialog() { - this.dialogOpen = false; - this.#dotBinaryFieldStore.setMode(BinaryFieldMode.DROPZONE); - } - - /** - * Open file picker - * - * @memberof DotEditContentBinaryFieldComponent - */ - openFilePicker() { - this.inputFile.nativeElement.click(); - } - - /** - * Handle file selection - * - * @param {Event} event - * @memberof DotEditContentBinaryFieldComponent - */ - handleFileSelection(event: Event) { - const input = event.target as HTMLInputElement; - const file = input.files[0]; - this.#dotBinaryFieldStore.handleUploadFile(file); - } - - /** - * Remove file - * - * @memberof DotEditContentBinaryFieldComponent - */ - removeFile() { - this.#dotBinaryFieldStore.removeFile(); - } - - /** - * Set temp file - * - * @param {DotCMSTempFile} tempFile - * @memberof DotEditContentBinaryFieldComponent - */ - setTempFile(tempFile: DotCMSTempFile) { - this.#dotBinaryFieldStore.setFileFromTemp(tempFile); - this.dialogOpen = false; - } - - /** - * Open Dialog to edit file in editor - * - * @memberof DotEditContentBinaryFieldComponent - */ - onEditFile() { - this.openDialog(BinaryFieldMode.EDITOR); - } - - /** - * Open Image Editor - * - * @memberof DotEditContentBinaryFieldComponent - */ - onEditImage() { - this.#dotBinaryFieldEditImageService.openImageEditor({ - inode: this.contentlet?.inode, - tempId: this.tempId, - variable: this.variable - }); - } - - /** - * Set drop zone active state - * - * @param {boolean} value - * @memberof DotEditContentBinaryFieldComponent - */ - setDropZoneActiveState(value: boolean) { - this.#dotBinaryFieldStore.setDropZoneActive(value); - } - - /** - * Handle file drop - * - * @param {DropZoneFileEvent} { validity, file } - * @return {*} - * @memberof DotEditContentBinaryFieldComponent - */ - handleFileDrop({ validity, file }: DropZoneFileEvent): void { - if (!validity.valid) { - this.handleFileDropError(validity); - - return; - } - - this.#dotBinaryFieldStore.handleUploadFile(file); - } - - /** - * Set field variables - * - * @private - * @memberof DotEditContentBinaryFieldComponent - */ - private setFieldVariables() { - const field = this.$field(); - const { - accept, - maxFileSize = 0, - systemOptions = `{ - "allowURLImport": true, - "allowCodeWrite": true, - "allowGenerateImg": true - }` - } = getFieldVariablesParsed<{ - accept: string; - maxFileSize: string; - systemOptions: string; - }>(field?.fieldVariables); - - this.#dotBinaryFieldValidatorService.setAccept(accept ? accept.split(',') : []); - this.#dotBinaryFieldValidatorService.setMaxFileSize(Number(maxFileSize)); - this.systemOptions.set(JSON.parse(systemOptions)); - this.#cd.detectChanges(); - } - - /** - * Handle file drop error - * - * @private - * @param {DropZoneFileValidity} { errorsType } - * @memberof DotEditContentBinaryFieldComponent - */ - private handleFileDropError({ errorsType }: DropZoneFileValidity): void { - const messageArgs = { - [DropZoneErrorType.FILE_TYPE_MISMATCH]: this.accept.join(', '), - [DropZoneErrorType.MAX_FILE_SIZE_EXCEEDED]: `${this.maxFileSize} bytes` - }; - const errorType = errorsType[0]; - const uiMessage = getUiMessage(errorType, messageArgs[errorType]); - - this.#dotBinaryFieldStore.invalidFile(uiMessage); - } - - /** - * Parses the custom Monaco options for a given field of a DotCMSContentTypeField. - * - * @returns {Record} Returns the parsed custom Monaco options as a key-value pair object. - * @private - * @param fieldVariables - */ - private parseCustomMonacoOptions( - fieldVariables: DotCMSContentTypeFieldVariable[] - ): Record { - const { monacoOptions } = getFieldVariablesParsed<{ monacoOptions: string }>( - fieldVariables - ); - - return stringToJson(monacoOptions); - } - - /** - * Check if the contentlet has metadata - * - * @private - * @return {*} {boolean} - * @memberof DotEditContentBinaryFieldComponent - */ - private checkMetadata(): boolean { - const { baseType } = this.contentlet; - const isFileAsset = baseType === DotCMSBaseTypesContentTypes.FILEASSET; - const key = isFileAsset ? 'metaData' : this.variable + 'MetaData'; - - return !!this.contentlet[key]; - } - - private parseToTempFile(selectedImage: DotGeneratedAIImage) { - const { response } = selectedImage; - const { contentlet } = response; - const metaData = contentlet['assetMetaData']; - - const tempFile: DotCMSTempFile = { - id: response.response, - fileName: response.tempFileName, - folder: contentlet.folder, - image: true, - length: metaData.length, - mimeType: metaData.contentType, - referenceUrl: contentlet.asset, - thumbnailUrl: contentlet.asset, - metadata: metaData - }; - - return tempFile; - } - - private getValue() { - if (this.value !== null) { - return this.value; - } - - return this.contentlet?.[this.variable] ?? this.$field().defaultValue; - } + } diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/interfaces/index.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/interfaces/index.ts deleted file mode 100644 index 09ac28d753ea..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/interfaces/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { DotFileMetadata } from '@dotcms/dotcms-models'; -import { DropZoneErrorType } from '@dotcms/ui'; - -export enum BinaryFieldMode { - DROPZONE = 'DROPZONE', - URL = 'URL', - EDITOR = 'EDITOR', - AI = 'AI' -} - -export enum BinaryFieldStatus { - INIT = 'INIT', - UPLOADING = 'UPLOADING', - PREVIEW = 'PREVIEW' -} - -export interface DotFilePreview extends DotFileMetadata { - id: string; - titleImage: string; - inode?: string; - url?: string; - content?: string; -} - -export enum UI_MESSAGE_KEYS { - DEFAULT = 'DEFAULT', - SERVER_ERROR = 'SERVER_ERROR' -} - -type BINARY_FIELD_MESSAGE_KEY = UI_MESSAGE_KEYS | DropZoneErrorType; - -export type UiMessageMap = { - [key in BINARY_FIELD_MESSAGE_KEY]: UiMessageI; -}; - -export interface UiMessageI { - message: string; - severity: string; - icon: string; - args?: string[]; -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/service/dot-binary-field-edit-image/dot-binary-field-edit-image.service.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/service/dot-binary-field-edit-image/dot-binary-field-edit-image.service.spec.ts deleted file mode 100644 index b434def8b22e..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/service/dot-binary-field-edit-image/dot-binary-field-edit-image.service.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { expect } from '@jest/globals'; -import { SpectatorService, createServiceFactory } from '@ngneat/spectator'; - -import { skip } from 'rxjs/operators'; - -import { DotBinaryFieldEditImageService } from './dot-binary-field-edit-image.service'; - -describe('DotBinaryFieldEditImageService', () => { - let spectator: SpectatorService; - - const createService = createServiceFactory({ - service: DotBinaryFieldEditImageService - }); - - let spyDispatchEvent: jest.SpyInstance; - let spyAddEventListener: jest.SpyInstance; - let spyRemoveEventListener: jest.SpyInstance; - - beforeEach(() => { - spyDispatchEvent = jest.spyOn(document, 'dispatchEvent'); - spyAddEventListener = jest.spyOn(document, 'addEventListener'); - spyRemoveEventListener = jest.spyOn(document, 'removeEventListener'); - spectator = createService(); - }); - - it('should listen to edited image', () => { - const detail = { - variable: 'test', - inode: '456', - tempId: '789' - }; - - const tempEventName = `binaryField-tempfile-${detail.variable}`; - const openEditorEventName = `binaryField-open-image-editor-${detail.variable}`; - const openImageCustomEvent = new CustomEvent(openEditorEventName, { detail }); - - spectator.service.openImageEditor(detail); - expect(spyDispatchEvent).toHaveBeenCalledWith(openImageCustomEvent); - expect(spyAddEventListener).toHaveBeenCalledWith(tempEventName, expect.any(Function)); - }); - it('should listen to edited image 2', () => { - const detail = { - variable: 'test', - inode: '456', - tempId: '789' - }; - - const tempEventName = `binaryField-tempfile-${detail.variable}`; - const openEditorEventName = `binaryField-open-image-editor-${detail.variable}`; - const openImageCustomEvent = new CustomEvent(openEditorEventName, { detail }); - - spectator.service.openImageEditor(detail); - expect(spyDispatchEvent).toHaveBeenCalledWith(openImageCustomEvent); - expect(spyAddEventListener).toHaveBeenCalledWith(tempEventName, expect.any(Function)); - }); - - it('should listen to edited image 3', () => { - const detail = { - variable: 'test', - inode: '456', - tempId: '789' - }; - - const tempEventName = `binaryField-tempfile-${detail.variable}`; - const openEditorEventName = `binaryField-open-image-editor-${detail.variable}`; - const openImageCustomEvent = new CustomEvent(openEditorEventName, { detail }); - - spectator.service.openImageEditor(detail); - expect(spyDispatchEvent).toHaveBeenCalledWith(openImageCustomEvent); - expect(spyAddEventListener).toHaveBeenCalledWith(tempEventName, expect.any(Function)); - }); - - it('should emit edited image and remove listener', (done) => { - const tempFile = { id: '123', url: 'http://example.com/image.jpg' }; - const data = { - variable: 'test', - inode: '456', - tempId: '789' - }; - - spectator.service - .editedImage() - .pipe(skip(1)) - .subscribe((file) => { - expect(file).toEqual(tempFile); - done(); - }); - - const tempEventName = `binaryField-tempfile-${data.variable}`; - const closeEventName = `binaryField-close-image-editor-${data.variable}`; - - spectator.service.openImageEditor(data); - document.dispatchEvent(new CustomEvent(tempEventName, { detail: { tempFile } })); - - expect(spyRemoveEventListener.mock.calls[0]).toEqual([tempEventName, expect.any(Function)]); - expect(spyRemoveEventListener.mock.calls[1]).toEqual([ - closeEventName, - expect.any(Function) - ]); - }); - - it('should listen to close image editor and remove listeners', () => { - const data = { - variable: 'test', - inode: '456', - tempId: '789' - }; - - const tempEventName = `binaryField-tempfile-${data.variable}`; - const closeEventName = `binaryField-close-image-editor-${data.variable}`; - - spectator.service.openImageEditor(data); - - document.dispatchEvent(new CustomEvent(closeEventName, {})); - expect(spyRemoveEventListener.mock.calls[0]).toEqual([tempEventName, expect.any(Function)]); - expect(spyRemoveEventListener.mock.calls[1]).toEqual([ - closeEventName, - expect.any(Function) - ]); - }); -}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/service/dot-binary-field-edit-image/dot-binary-field-edit-image.service.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/service/dot-binary-field-edit-image/dot-binary-field-edit-image.service.ts deleted file mode 100644 index e590d1ad3d1a..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/service/dot-binary-field-edit-image/dot-binary-field-edit-image.service.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { BehaviorSubject, Observable } from 'rxjs'; - -import { Injectable } from '@angular/core'; - -import { DotCMSTempFile } from '@dotcms/dotcms-models'; - -interface ImageEditorProps { - inode: string; - tempId: string; - variable: string; -} - -@Injectable() -export class DotBinaryFieldEditImageService { - private subject: BehaviorSubject = new BehaviorSubject(null); - private variable: string; - - editedImage(): Observable { - return this.subject.asObservable(); - } - - /** - * Open the dojo image editor modal and listen to the edited image - * - * @param {ImageEditorProps} { inode, tempId, variable } - * @memberof DotBinaryFieldEditImageService - */ - openImageEditor({ inode, tempId, variable }: ImageEditorProps): void { - this.variable = variable; - const customEvent = new CustomEvent(`binaryField-open-image-editor-${variable}`, { - detail: { - inode, - tempId, - variable - } - }); - document.dispatchEvent(customEvent); - this.listenToEditedImage(); - this.listenToCloseImageEditor(); - } - - /** - * Listen to the edited image - * - * @memberof DotBinaryFieldEditImageService - */ - listenToEditedImage(): void { - document.addEventListener( - `binaryField-tempfile-${this.variable}`, - this.handleNewImage.bind(this) - ); - } - - /** - * Listen to the close image editor event - * - * @memberof DotBinaryFieldEditImageService - */ - listenToCloseImageEditor(): void { - document.addEventListener( - `binaryField-close-image-editor-${this.variable}`, - this.removeListener.bind(this) - ); - } - - /** - * Remove the listener to the edited image - * - * @memberof DotBinaryFieldEditImageService - */ - removeListener(): void { - document.removeEventListener( - `binaryField-tempfile-${this.variable}`, - this.handleNewImage.bind(this) - ); - - document.removeEventListener( - `binaryField-close-image-editor-${this.variable}`, - this.removeListener.bind(this) - ); - } - - /** - * Handle the edited image - * - * @private - * @param {*} { detail } - * @memberof DotBinaryFieldEditImageService - */ - private handleNewImage({ detail }): void { - this.subject.next(detail.tempFile); - this.removeListener(); - } -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/service/dot-binary-field-validator/dot-binary-field-validator.service.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/service/dot-binary-field-validator/dot-binary-field-validator.service.spec.ts deleted file mode 100644 index 83c7fba9e82f..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/service/dot-binary-field-validator/dot-binary-field-validator.service.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { createServiceFactory, SpectatorService } from '@ngneat/spectator'; - -import { DotBinaryFieldValidatorService } from './dot-binary-field-validator.service'; - -describe('DotBinaryFieldValidatorService', () => { - let spectator: SpectatorService; - const createService = createServiceFactory(DotBinaryFieldValidatorService); - - beforeEach(() => (spectator = createService())); - - it('should validate file type', () => { - spectator.service.setAccept(['image/*', '.ts']); - expect( - spectator.service.isValidType({ extension: 'jpg', mimeType: 'image/jpeg' }) - ).toBeTruthy(); - expect( - spectator.service.isValidType({ extension: 'ts', mimeType: 'text/typescript' }) - ).toBeTruthy(); - expect( - spectator.service.isValidType({ extension: 'doc', mimeType: 'application/msword' }) - ).toBeFalsy(); - }); - - it('should validate file size', () => { - spectator.service.setMaxFileSize(5000); - expect(spectator.service.isValidSize(3000)).toBeTruthy(); - expect(spectator.service.isValidSize(6000)).toBeFalsy(); - }); -}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/service/dot-binary-field-validator/dot-binary-field-validator.service.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/service/dot-binary-field-validator/dot-binary-field-validator.service.ts deleted file mode 100644 index e1368d726f12..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/service/dot-binary-field-validator/dot-binary-field-validator.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable() -export class DotBinaryFieldValidatorService { - #maxFileSize: number; - #accept: string[] = []; - #acceptSanitized: string[] = []; - - get accept(): string[] { - return this.#accept; - } - - get maxFileSize(): number { - return this.#maxFileSize; - } - - setMaxFileSize(maxFileSize: number) { - this.#maxFileSize = maxFileSize; - } - - setAccept(accept: string[]) { - this.#accept = accept; - this.#acceptSanitized = accept - ?.filter((value) => value !== '*/*') - .map((type) => { - // Remove the wildcard character - return type.toLowerCase().replace(/\*/g, ''); - }); - } - - isValidType({ extension, mimeType }): boolean { - if (this.#acceptSanitized?.length === 0) { - return true; - } - - const sanitizedExtension = extension?.replace('.', ''); - - return this.#acceptSanitized.some( - (type) => mimeType?.includes(type) || type?.includes(`.${sanitizedExtension}`) - ); - } - - isValidSize(size: number): boolean { - if (!this.#maxFileSize) { - return true; - } - - return size <= this.#maxFileSize; - } -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.spec.ts deleted file mode 100644 index a84f87866a07..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.spec.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { expect, describe } from '@jest/globals'; -import { HttpMethod, SpectatorService, createServiceFactory } from '@ngneat/spectator'; -import { of } from 'rxjs'; - -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; - -import { skip } from 'rxjs/operators'; - -import { DotLicenseService, DotUploadService } from '@dotcms/data-access'; -import { DotCMSTempFile } from '@dotcms/dotcms-models'; -import { DropZoneErrorType } from '@dotcms/ui'; - -import { BinaryFieldState, DotBinaryFieldStore } from './file-field.store'; - -import { BINARY_FIELD_CONTENTLET } from '../../../utils/mocks'; -import { BinaryFieldMode, BinaryFieldStatus, UI_MESSAGE_KEYS } from '../interfaces'; -import { getUiMessage } from '../utils/binary-field-utils'; -import { fileMetaData } from '../utils/mock'; - -const INITIAL_STATE: BinaryFieldState = { - contentlet: null, - tempFile: null, - value: null, - mode: BinaryFieldMode.DROPZONE, - status: BinaryFieldStatus.INIT, - uiMessage: getUiMessage(UI_MESSAGE_KEYS.DEFAULT), - dropZoneActive: false, - isEnterprise: false -}; - -export const TEMP_FILE_MOCK: DotCMSTempFile = { - content: 'test', - fileName: 'image.png', - folder: '/images', - id: '12345', - image: true, - length: 1000, - referenceUrl: '/reference/url', - thumbnailUrl: 'image.png', - mimeType: 'mimeType', - metadata: fileMetaData -}; - -describe('DotBinaryFieldStore', () => { - let spectator: SpectatorService; - let store: DotBinaryFieldStore; - let httpMock: HttpTestingController; - - let dotUploadService: DotUploadService; - let initialState; - - const createStoreService = createServiceFactory({ - service: DotBinaryFieldStore, - imports: [HttpClientTestingModule], - providers: [ - { - provide: DotLicenseService, - useValue: { - isEnterprise: () => of(true) - } - }, - { - provide: DotUploadService, - useValue: { - uploadFile: ({ file }) => { - return new Promise((resolve) => { - if (file) { - resolve(TEMP_FILE_MOCK); - } - }); - } - } - } - ] - }); - - beforeEach(() => { - spectator = createStoreService(); - store = spectator.inject(DotBinaryFieldStore); - dotUploadService = spectator.inject(DotUploadService); - httpMock = spectator.inject(HttpTestingController); - - store.setState(INITIAL_STATE); - store.state$.subscribe((state) => { - initialState = state; - }); - }); - - it('should set initial state', () => { - expect(initialState).toEqual(INITIAL_STATE); - }); - - describe('Updaters', () => { - it('should set Contentlet', (done) => { - store.setContentlet(BINARY_FIELD_CONTENTLET); - - store.vm$.subscribe((state) => { - expect(state.contentlet).toEqual(BINARY_FIELD_CONTENTLET); - expect(state.value).toEqual(BINARY_FIELD_CONTENTLET.value); - done(); - }); - }); - - it('should set TempFile', (done) => { - store.setTempFile(TEMP_FILE_MOCK); - - store.vm$.subscribe((state) => { - expect(state.tempFile).toEqual(TEMP_FILE_MOCK); - expect(state.value).toEqual(TEMP_FILE_MOCK.id); - done(); - }); - }); - - it('should set uiMessage', (done) => { - const uiMessage = getUiMessage(DropZoneErrorType.FILE_TYPE_MISMATCH); - store.setUiMessage(uiMessage); - - store.vm$.subscribe((state) => { - expect(state.uiMessage).toEqual(uiMessage); - done(); - }); - }); - - it('should set Mode', (done) => { - store.setMode(BinaryFieldMode.EDITOR); - - store.vm$.subscribe((state) => { - expect(state.mode).toBe(BinaryFieldMode.EDITOR); - done(); - }); - }); - - it('should set Status', (done) => { - store.setStatus(BinaryFieldStatus.PREVIEW); - - store.vm$.subscribe((state) => { - expect(state.status).toBe(BinaryFieldStatus.PREVIEW); - done(); - }); - }); - - it('should set DropZoneActive', (done) => { - store.setDropZoneActive(true); - - store.vm$.subscribe((state) => { - expect(state.dropZoneActive).toBe(true); - done(); - }); - }); - }); - - describe('Actions', () => { - describe('handleUploadFile', () => { - it('should set value from tempFile and status to PREVIEW when dropping a valid', (done) => { - const file = new File([''], 'filename'); - const spyUploading = jest.spyOn(store, 'setUploading'); - - store.handleUploadFile(file); - - // Skip initial state - store.value$.pipe(skip(1)).subscribe((value) => { - expect(value).toEqual({ - value: TEMP_FILE_MOCK.id, - fileName: TEMP_FILE_MOCK.fileName - }); - done(); - }); - - expect(spyUploading).toHaveBeenCalled(); - }); - - it('should called tempFile API with 1MB', (done) => { - const file = new File([''], 'filename'); - const spyOnUploadService = jest.spyOn(dotUploadService, 'uploadFile'); - - // 1MB - store.setMaxFileSize(1048576); - store.handleUploadFile(file); - - // Skip initial state - store.value$.pipe(skip(1)).subscribe(() => { - expect(spyOnUploadService).toHaveBeenCalledWith({ - file, - maxSize: '1MB' - }); - done(); - }); - }); - }); - }); - - describe('Effects', () => { - describe('setFileFromContentlet', () => { - it(`should get content if the file is editableAsText`, (done) => { - const { BinaryMetaData } = BINARY_FIELD_CONTENTLET; - const metaData = { - ...BinaryMetaData, - editableAsText: true - }; - - const NEW_BINARY_FIELD_CONTENTLET = { - ...BINARY_FIELD_CONTENTLET, - fileAssetVersion: '12345', - metaData - }; - - store.setFileFromContentlet(NEW_BINARY_FIELD_CONTENTLET); - - const req = httpMock.expectOne('12345', HttpMethod.GET); // Need to check here - req.flush('DATA'); // Need to flush here - - store.state$.subscribe((state) => { - expect(state.contentlet).toEqual({ - ...NEW_BINARY_FIELD_CONTENTLET, - mimeType: metaData.contentType, - name: metaData.name, - content: 'DATA' - }); - done(); - }); - }); - - it('should not get content if the file is not editableAsText', (done) => { - store.setFileFromContentlet({ - ...BINARY_FIELD_CONTENTLET, - metaData: { - ...fileMetaData, - editableAsText: false - } - }); - - store.state$.subscribe(() => { - httpMock.expectNone('test-url', HttpMethod.GET); - done(); - }); - }); - }); - - describe('setFileFromTemp', () => { - it(`should get content if the file is editableAsText`, (done) => { - const NEW_TEMP_FILE_MOCK = { - ...TEMP_FILE_MOCK, - metadata: { - ...fileMetaData, - editableAsText: true - } - }; - store.setFileFromTemp(NEW_TEMP_FILE_MOCK); - - const req = httpMock.expectOne(TEMP_FILE_MOCK.referenceUrl, HttpMethod.GET); // Need to check here - req.flush('DATA'); // Need to flush here - - store.state$.subscribe((state) => { - expect(state.tempFile).toEqual({ - ...NEW_TEMP_FILE_MOCK, - content: 'DATA' - }); - done(); - }); - }); - - it('should not get content if the file is not editableAsText', (done) => { - store.setFileFromTemp({ - ...TEMP_FILE_MOCK, - metadata: fileMetaData - }); - - store.state$.subscribe(() => { - httpMock.expectNone('test-url', HttpMethod.GET); - done(); - }); - }); - }); - }); -}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.ts deleted file mode 100644 index 654121c912c2..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/store/file-field.store.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { ComponentStore, tapResponse } from '@ngrx/component-store'; -import { from, Observable, of } from 'rxjs'; - -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; - -import { switchMap, tap, map, catchError, distinctUntilChanged } from 'rxjs/operators'; - -import { DotLicenseService, DotUploadService } from '@dotcms/data-access'; -import { DotCMSContentlet, DotCMSTempFile } from '@dotcms/dotcms-models'; - -import { - BinaryFieldMode, - BinaryFieldStatus, - UI_MESSAGE_KEYS, - UiMessageI -} from '../interfaces/index'; -import { getFieldVersion, getFileMetadata, getUiMessage } from '../utils/binary-field-utils'; - -export interface BinaryFieldState { - contentlet: DotCMSContentlet; - tempFile: DotCMSTempFile; - value: string; - mode: BinaryFieldMode; - status: BinaryFieldStatus; - uiMessage: UiMessageI; - dropZoneActive: boolean; - isEnterprise: boolean; -} - -const initialState: BinaryFieldState = { - contentlet: null, - tempFile: null, - value: null, - mode: BinaryFieldMode.DROPZONE, - status: BinaryFieldStatus.INIT, - dropZoneActive: false, - uiMessage: getUiMessage(UI_MESSAGE_KEYS.DEFAULT), - isEnterprise: false -}; - -@Injectable() -export class DotBinaryFieldStore extends ComponentStore { - private _maxFileSizeInMB = 0; - - get maxFile() { - return this._maxFileSizeInMB ? `${this._maxFileSizeInMB}MB` : ''; - } - - // Selectors - readonly vm$ = this.select((state) => ({ - ...state, - isLoading: state.status === BinaryFieldStatus.UPLOADING - })); - - readonly value$ = this.select(({ value, tempFile }) => ({ - value, - fileName: tempFile?.fileName - })).pipe(distinctUntilChanged((previous, current) => previous.value === current.value)); - - constructor( - private readonly dotUploadService: DotUploadService, - private readonly dotLicenseService: DotLicenseService, - private readonly http: HttpClient - ) { - super(initialState); - this.dotLicenseService.isEnterprise().subscribe((isEnterprise) => { - this.setIsEnterprise(isEnterprise); - }); - } - - readonly setDropZoneActive = this.updater((state, dropZoneActive) => ({ - ...state, - dropZoneActive - })); - - readonly setContentlet = this.updater((state, contentlet) => ({ - ...state, - contentlet, - status: BinaryFieldStatus.PREVIEW, - value: contentlet?.value || '' - })); - - readonly setTempFile = this.updater((state, tempFile) => ({ - ...state, - tempFile, - contentlet: null, - status: BinaryFieldStatus.PREVIEW, - value: tempFile?.id - })); - - readonly setValue = this.updater((state, value) => ({ - ...state, - value - })); - - readonly setUiMessage = this.updater((state, uiMessage) => ({ - ...state, - uiMessage - })); - - readonly setMode = this.updater((state, mode) => ({ - ...state, - mode - })); - - readonly setStatus = this.updater((state, status) => ({ - ...state, - status - })); - - readonly setIsEnterprise = this.updater((state, isEnterprise) => ({ - ...state, - isEnterprise - })); - - readonly setUploading = this.updater((state) => ({ - ...state, - dropZoneActive: false, - uiMessage: getUiMessage(UI_MESSAGE_KEYS.DEFAULT), - status: BinaryFieldStatus.UPLOADING - })); - - readonly setError = this.updater((state, uiMessage) => ({ - ...state, - uiMessage, - status: BinaryFieldStatus.INIT, - tempFile: null - })); - - readonly invalidFile = this.updater((state, uiMessage) => ({ - ...state, - dropZoneActive: false, - uiMessage, - status: BinaryFieldStatus.INIT - })); - - readonly removeFile = this.updater((state) => ({ - ...state, - contentlet: null, - tempFile: null, - value: '', - status: BinaryFieldStatus.INIT - })); - - // Effects - readonly handleUploadFile = this.effect((file$: Observable) => - file$.pipe( - tap(() => this.setUploading()), - switchMap((file) => - this.uploadFile(file).pipe( - switchMap((tempFile) => this.handleTempFile(tempFile)), - tap((file) => this.setTempFile(file)), - catchError(() => { - this.setError(getUiMessage(UI_MESSAGE_KEYS.SERVER_ERROR)); - - return of(null); - }) - ) - ) - ) - ); - - readonly setFileFromTemp = this.effect((file$: Observable) => { - return file$.pipe( - tap(() => this.setUploading()), - - switchMap((tempFile) => { - return this.handleTempFile(tempFile).pipe( - tapResponse( - (file) => this.setTempFile(file), - () => this.setError(getUiMessage(UI_MESSAGE_KEYS.SERVER_ERROR)) - ) - ); - }) - ); - }); - - readonly setFileFromContentlet = this.effect( - (contentlet$: Observable) => { - return contentlet$.pipe( - tap(() => this.setUploading()), - switchMap((contentlet) => { - const { contentType, editableAsText, name } = getFileMetadata(contentlet); - const contentURL = getFieldVersion(contentlet); - const obs$ = editableAsText ? this.getFileContent(contentURL) : of(''); - - return obs$.pipe( - tapResponse( - (content = '') => { - this.setContentlet({ - ...contentlet, - mimeType: contentType, - name, - content - }); - }, - () => { - this.setContentlet({ - ...contentlet, - mimeType: contentType, - name - }); - } - ) - ); - }) - ); - } - ); - - private handleTempFile(tempFile: DotCMSTempFile): Observable { - const { referenceUrl, metadata } = tempFile; - const { editableAsText = false } = metadata; - - const obs$ = editableAsText ? this.getFileContent(referenceUrl) : of(''); - - return obs$.pipe( - map((content) => { - return { - ...tempFile, - content - }; - }) - ); - } - - /** - * Set the max file size in bytes - * - * @param bytes The max file size in bytes - */ - setMaxFileSize(bytes: number): void { - // Convert bytes to MB - this._maxFileSizeInMB = bytes / (1024 * 1024); - } - - private uploadFile(file: File): Observable { - return from( - this.dotUploadService.uploadFile({ - file, - maxSize: this.maxFile - }) - ); - } - - /** - * Get the file content - * - * @private - * @param {string} url - * @return {*} {Observable} - * @memberof DotBinaryFieldStore - */ - private getFileContent(url: string): Observable { - return this.http.get(url, { responseType: 'text' }); - } -} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/binary-field-utils.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/binary-field-utils.ts deleted file mode 100644 index bb216eb7eac1..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/binary-field-utils.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { DotCMSContentlet } from '@dotcms/dotcms-models'; - -import { UiMessageI, UiMessageMap } from '../interfaces'; - -const UiMessageMap: UiMessageMap = { - DEFAULT: { - message: 'dot.binary.field.drag.and.drop.message', - severity: 'info', - icon: 'pi pi-upload' - }, - SERVER_ERROR: { - message: 'dot.binary.field.drag.and.drop.error.server.error.message', - severity: 'error', - icon: 'pi pi-exclamation-triangle' - }, - FILE_TYPE_MISMATCH: { - message: 'dot.binary.field.drag.and.drop.error.file.not.supported.message', - severity: 'error', - icon: 'pi pi-exclamation-triangle' - }, - MAX_FILE_SIZE_EXCEEDED: { - message: 'dot.binary.field.drag.and.drop.error.file.maxsize.exceeded.message', - severity: 'error', - icon: 'pi pi-exclamation-triangle' - }, - MULTIPLE_FILES_DROPPED: { - message: 'dot.binary.field.drag.and.drop.error.multiple.files.dropped.message', - severity: 'error', - icon: 'pi pi-exclamation-triangle' - } -}; - -export const getUiMessage = (messageKey: string, ...args: string[]): UiMessageI => { - return { - ...UiMessageMap[messageKey], - args - }; -}; - -export const getFileMetadata = (contentlet: DotCMSContentlet) => { - const { metaData, fieldVariable } = contentlet; - - const metadata = metaData || contentlet[`${fieldVariable}MetaData`]; - - return metadata || {}; -}; - -export const getFieldVersion = (contentlet: DotCMSContentlet) => { - const { fileAssetVersion, fieldVariable } = contentlet; - - return fileAssetVersion || contentlet[`${fieldVariable}Version`]; -}; diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/mock.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/mock.ts deleted file mode 100644 index e8d78a46e472..000000000000 --- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/mock.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { DotCMSTempFile } from '@dotcms/dotcms-models'; -import { MockDotMessageService } from '@dotcms/utils-testing'; - -const MESSAGES_MOCK = { - 'dot.binary.field.action.choose.file': 'Choose File', - 'dot.binary.field.action.create.new.file': 'Create New File', - 'dot.binary.field.action.create.new.file.label': 'File Name', - 'dot.binary.field.action.import.from.url.error.message': - 'The URL you requested is not valid. Please try again.', - 'dot.binary.field.action.import.from.url': 'Import from URL', - 'dot.binary.field.action.remove': 'Remove', - 'dot.binary.field.dialog.create.new.file.header': 'File Details', - 'dot.binary.field.dialog.import.from.url.header': 'URL', - 'dot.binary.field.drag.and.drop.error.could.not.load.message': - 'Couldn't load the file. Please try again or', - 'dot.binary.field.drag.and.drop.error.file.maxsize.exceeded.message': - 'The file weight exceeds the limits of {0}, please reduce size before uploading.', - 'dot.binary.field.drag.and.drop.error.file.not.supported.message': - 'This type of file is not supported, Please select a {0} file.', - 'dot.binary.field.drag.and.drop.error.multiple.files.dropped.message': - 'You can only upload one file at a time.', - 'dot.binary.field.drag.and.drop.error.server.error.message': - 'Something went wrong, please try again or contact our support team.', - 'dot.binary.field.drag.and.drop.message': 'Drag and Drop or', - 'dot.binary.field.error.type.file.not.extension': "Please add the file's extension", - 'dot.binary.field.error.type.file.not.supported.message': - 'This type of file is not supported. Please use a {0} file.', - 'dot.binary.field.file.bytes': 'Bytes', - 'dot.binary.field.file.dimension': 'Dimension', - 'dot.binary.field.file.size': 'File Size', - 'dot.binary.field.import.from.url.error.file.not.supported.message': - 'This type of file is not supported, Please import a {0} file.', - 'dot.common.cancel': 'Cancel', - 'dot.common.edit': 'Edit', - 'dot.common.import': 'Import', - 'dot.common.remove': 'Remove', - 'dot.common.save': 'Save', - 'error.form.validator.required': 'This field is required' -}; - -export const CONTENTTYPE_FIELDS_MESSAGE_MOCK = new MockDotMessageService(MESSAGES_MOCK); - -const TEMP_IMAGE_MOCK: DotCMSTempFile = { - fileName: 'Image.jpg', - folder: 'folder', - id: 'tempFileId', - image: true, - length: 10000, - mimeType: 'image/jpeg', - referenceUrl: '', - thumbnailUrl: - 'https://images.unsplash.com/photo-1575936123452-b67c3203c357?auto=format&fit=crop&q=80&w=1000&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8aW1hZ2V8ZW58MHx8MHx8fDA%3D', - metadata: { - contentType: 'image/jpeg', - fileSize: 12312, - length: 12312, - isImage: true, - modDate: 12312, - name: 'image.png', - sha256: '12345', - title: 'Asset', - version: 1, - height: 100, - width: 100, - editableAsText: false - } -}; - -const TEMP_VIDEO_MOCK = { - fileName: 'video.mp4', - folder: 'folder', - id: 'tempFileId', - image: false, - length: 10000, - mimeType: 'video/mp4', - referenceUrl: 'https://www.w3schools.com/tags/movie.mp4', - thumbnailUrl: '' -}; - -const TEMP_FILE_MOCK = { - fileName: 'template.html', - folder: 'folder', - id: 'tempFileId', - image: false, - length: 10000, - mimeType: 'text/html', - referenceUrl: 'https://raw.githubusercontent.com/angular/angular/master/README.md', - thumbnailUrl: '', - content: 'HOLA' -}; - -export const TEMP_FILES_MOCK = [TEMP_IMAGE_MOCK, TEMP_VIDEO_MOCK, TEMP_FILE_MOCK]; - -export const CONTENTLET = { - publishDate: '2023-10-24 13:21:49.682', - inode: 'b22aa2f3-12af-4ea8-9d7d-164f98ea30b1', - binaryField2: '/dA/af9294c29906dea7f4a58d845f569219/binaryField2/New-Image.png', - host: '48190c8c-42c4-46af-8d1a-0cd5db894797', - binaryField2Version: '/dA/b22aa2f3-12af-4ea8-9d7d-164f98ea30b1/binaryField2/New-Image.png', - locked: false, - stInode: 'd1901a41d38b6686dd5ed8f910346d7a', - contentType: 'BinaryField', - identifier: 'af9294c29906dea7f4a58d845f569219', - folder: 'SYSTEM_FOLDER', - hasTitleImage: true, - sortOrder: 0, - binaryField2MetaData: { - modDate: 1698153707197, - sha256: 'e84030fe91978e483e34242f0631a81903cf53a945475d8dcfbb72da484a28d5', - length: 29848, - title: 'New-Image.png', - version: 20220201, - isImage: true, - fileSize: 29848, - name: 'New-Image.png', - width: 738, - contentType: 'image/png', - height: 435 - }, - hostName: 'demo.dotcms.com', - modDate: '2023-10-24 13:21:49.682', - title: 'af9294c29906dea7f4a58d845f569219', - baseType: 'CONTENT', - archived: false, - working: true, - live: true, - owner: 'dotcms.org.1', - binaryField2ContentAsset: 'af9294c29906dea7f4a58d845f569219/binaryField2', - languageId: 1, - url: '/content.b22aa2f3-12af-4ea8-9d7d-164f98ea30b1', - titleImage: 'binaryField2', - modUserName: 'Admin User', - hasLiveVersion: true, - modUser: 'dotcms.org.1', - __icon__: 'contentIcon', - contentTypeIcon: 'event_note', - variant: 'DEFAULT' -}; - -export const fileMetaData = { - contentType: 'image/png', - fileSize: 12312, - length: 12312, - modDate: 12312, - name: 'image.png', - sha256: '12345', - title: 'Asset', - version: 1, - height: 100, - width: 100, - editableAsText: false, - isImage: true -}; diff --git a/core-web/libs/edit-content/src/lib/models/dot-edit-content-field.enum.ts b/core-web/libs/edit-content/src/lib/models/dot-edit-content-field.enum.ts index 8ac24de40057..965105c1b84d 100644 --- a/core-web/libs/edit-content/src/lib/models/dot-edit-content-field.enum.ts +++ b/core-web/libs/edit-content/src/lib/models/dot-edit-content-field.enum.ts @@ -10,6 +10,7 @@ export enum DotEditContentFieldSingleSelectableDataType { // Map to match the field type to component selector export enum FIELD_TYPES { BINARY = 'Binary', + FILE = 'File', BLOCK_EDITOR = 'Story-Block', CATEGORY = 'Category', CHECKBOX = 'Checkbox',