diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-file-editor/dot-form-file-editor.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-file-editor/dot-form-file-editor.component.html
new file mode 100644
index 000000000000..aff434f107ef
--- /dev/null
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-file-editor/dot-form-file-editor.component.html
@@ -0,0 +1,60 @@
+
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-file-editor/dot-form-file-editor.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-file-editor/dot-form-file-editor.component.scss
new file mode 100644
index 000000000000..ea37a78b38f9
--- /dev/null
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-file-editor/dot-form-file-editor.component.scss
@@ -0,0 +1,93 @@
+@use "variables" as *;
+
+:host ::ng-deep {
+ .editor-container {
+ position: absolute !important;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ }
+}
+
+.file-field__editor-container {
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ flex-direction: column;
+ flex: 1;
+ width: 100%;
+ gap: $spacing-1;
+}
+
+.file-field__code-editor {
+ border: 1px solid $color-palette-gray-400; // Input
+ display: block;
+ flex-grow: 1;
+ width: 100%;
+ min-height: 30rem;
+ border-radius: $border-radius-md;
+ overflow: auto;
+ position: relative;
+}
+
+.file-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-form-file-editor/dot-form-file-editor.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-file-editor/dot-form-file-editor.component.ts
new file mode 100644
index 000000000000..edc56360c0ed
--- /dev/null
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-file-editor/dot-form-file-editor.component.ts
@@ -0,0 +1,275 @@
+import { MonacoEditorConstructionOptions, MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
+
+import {
+ ChangeDetectionStrategy,
+ Component,
+ effect,
+ inject,
+ OnInit,
+ untracked
+} from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
+
+import { ButtonModule } from 'primeng/button';
+import { DynamicDialogRef, DynamicDialogConfig } from 'primeng/dynamicdialog';
+import { InputTextModule } from 'primeng/inputtext';
+
+import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
+
+import { DotMessagePipe, DotFieldValidationMessageComponent } from '@dotcms/ui';
+
+import { FormFileEditorStore } from './store/form-file-editor.store';
+
+import { UploadedFile } from '../../models';
+
+type DialogProps = {
+ allowFileNameEdit: boolean;
+ userMonacoOptions: Partial;
+ uploadedFile: UploadedFile | null;
+};
+
+@Component({
+ selector: 'dot-form-file-editor',
+ standalone: true,
+ imports: [
+ DotMessagePipe,
+ ReactiveFormsModule,
+ DotFieldValidationMessageComponent,
+ ButtonModule,
+ InputTextModule,
+ MonacoEditorModule
+ ],
+ templateUrl: './dot-form-file-editor.component.html',
+ styleUrls: ['./dot-form-file-editor.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [FormFileEditorStore]
+})
+export class DotFormFileEditorComponent implements OnInit {
+ /**
+ * Injects the FormFileEditorStore into the component.
+ *
+ * @readonly
+ * @type {FormFileEditorStore}
+ */
+ readonly store = inject(FormFileEditorStore);
+ /**
+ * A private and readonly instance of FormBuilder injected into the component.
+ * This instance is used to create and manage forms within the component.
+ */
+ readonly #formBuilder = inject(FormBuilder);
+ /**
+ * A reference to the dynamic dialog instance.
+ * This is a read-only property that is injected using the `DynamicDialogRef` token.
+ */
+ readonly #dialogRef = inject(DynamicDialogRef);
+ /**
+ * A read-only private property that holds the configuration for the dynamic dialog.
+ * This configuration is injected using the `DynamicDialogConfig` class with a generic type of `DialogProps`.
+ */
+ readonly #dialogConfig = inject(DynamicDialogConfig);
+
+ /**
+ * Form group for the file editor component.
+ *
+ * This form contains the following controls:
+ * - `name`: A required string field that must match the pattern of a valid file name (e.g., "filename.extension").
+ * - `content`: An optional string field for the file content.
+ *
+ * @readonly
+ */
+ readonly form = this.#formBuilder.nonNullable.group({
+ name: ['', [Validators.required, Validators.pattern(/^[^.]+\.[^.]+$/)]],
+ content: ['']
+ });
+
+ /**
+ * Reference to the MonacoEditorComponent instance within the view.
+ * This is used to interact with the Monaco Editor component in the template.
+ *
+ * @type {MonacoEditorComponent}
+ */
+ #editorRef: monaco.editor.IStandaloneCodeEditor | null = null;
+
+ constructor() {
+ effect(() => {
+ const isUploading = this.store.isUploading();
+
+ if (isUploading) {
+ this.#disableEditor();
+ } else {
+ this.#enableEditor();
+ }
+ });
+
+ effect(
+ () => {
+ const isDone = this.store.isDone();
+ const uploadedFile = this.store.uploadedFile();
+
+ untracked(() => {
+ if (isDone) {
+ this.#dialogRef.close(uploadedFile);
+ }
+ });
+ },
+ {
+ allowSignalWrites: true
+ }
+ );
+
+ this.nameField.valueChanges
+ .pipe(
+ debounceTime(350),
+ distinctUntilChanged(),
+ filter((value) => value.length > 0),
+ takeUntilDestroyed()
+ )
+ .subscribe((value) => {
+ this.store.setFileName(value);
+ });
+ }
+
+ /**
+ * Initializes the component by extracting data from the dialog configuration
+ * and setting up the form and store with the provided values.
+ *
+ * @returns {void}
+ *
+ * @memberof DotFormFileEditorComponent
+ *
+ * @method ngOnInit
+ *
+ * @description
+ * This method is called once the component is initialized. It retrieves the
+ * dialog configuration data and initializes the form with the uploaded file
+ * if available. It also sets up the store with the provided Monaco editor
+ * options, file name edit permission, uploaded file, accepted files, and
+ * upload type.
+ */
+ ngOnInit(): void {
+ const data = this.#dialogConfig?.data as DialogProps;
+ if (!data) {
+ return;
+ }
+
+ const { uploadedFile, userMonacoOptions, allowFileNameEdit } = data;
+
+ if (uploadedFile) {
+ this.#initValuesForm(uploadedFile);
+ }
+
+ this.store.initLoad({
+ monacoOptions: userMonacoOptions || {},
+ allowFileNameEdit: allowFileNameEdit || true,
+ uploadedFile,
+ acceptedFiles: [],
+ uploadType: 'dotasset'
+ });
+ }
+
+ /**
+ * Handles the form submission event.
+ *
+ * This method performs the following actions:
+ * 1. Checks if the form is invalid. If so, marks the form as dirty and updates its validity status.
+ * 2. If the form is valid, retrieves the raw values from the form and triggers the file upload process via the store.
+ *
+ * @returns {void}
+ */
+ onSubmit(): void {
+ if (this.form.invalid) {
+ this.form.markAsDirty();
+ this.form.updateValueAndValidity();
+
+ return;
+ }
+
+ const values = this.form.getRawValue();
+ this.store.uploadFile(values);
+ }
+
+ /**
+ * Getter for the 'name' field control from the form.
+ *
+ * @returns The form control associated with the 'name' field.
+ */
+ get nameField() {
+ return this.form.controls.name;
+ }
+
+ /**
+ * Getter for the 'content' form control.
+ *
+ * @returns The 'content' form control from the form group.
+ */
+ get contentField() {
+ return this.form.controls.content;
+ }
+
+ /**
+ * Disables the form and sets the editor to read-only mode.
+ *
+ * This method disables the form associated with the component and updates the editor's options
+ * to make it read-only. It is useful for preventing further user interaction with the form and editor.
+ *
+ * @private
+ */
+ #disableEditor() {
+ if (!this.#editorRef) {
+ return;
+ }
+
+ this.form.disable();
+ this.#editorRef.updateOptions({ readOnly: true });
+ }
+
+ /**
+ * Enables the form and sets the editor to be editable.
+ *
+ * This method performs the following actions:
+ * 1. Enables the form associated with this component.
+ * 2. Retrieves the editor instance from the `$editorRef` method.
+ * 3. Updates the editor options to make it writable (readOnly: false).
+ */
+ #enableEditor() {
+ if (!this.#editorRef) {
+ return;
+ }
+
+ this.form.enable();
+ this.#editorRef.updateOptions({ readOnly: false });
+ }
+
+ /**
+ * Initializes the form values with the provided uploaded file data.
+ *
+ * @param {UploadedFile} param0 - The uploaded file object containing source and file information.
+ * @param {string} param0.source - The source of the file, which can be 'temp' or another value.
+ * @param {File} param0.file - The file object containing file details.
+ * @param {string} param0.file.fileName - The name of the file if the source is 'temp'.
+ * @param {string} param0.file.title - The title of the file if the source is not 'temp'.
+ * @param {string} param0.file.content - The content of the file.
+ */
+ #initValuesForm({ source, file }: UploadedFile): void {
+ this.form.patchValue({
+ name: source === 'temp' ? file.fileName : file.title,
+ content: file.content
+ });
+ }
+
+ /**
+ * Cancels the current file upload and closes the dialog.
+ *
+ * @remarks
+ * This method is used to terminate the ongoing file upload process and
+ * close the associated dialog reference.
+ */
+ cancelUpload(): void {
+ this.#dialogRef.close();
+ }
+
+ onEditorInit(editor: monaco.editor.IStandaloneCodeEditor) {
+ this.#editorRef = editor;
+ }
+}
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-file-editor/dot-form-file-editor.conts.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-file-editor/dot-form-file-editor.conts.ts
new file mode 100644
index 000000000000..db6cdf79463b
--- /dev/null
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-file-editor/dot-form-file-editor.conts.ts
@@ -0,0 +1,17 @@
+import { MonacoEditorConstructionOptions } from '@materia-ui/ngx-monaco-editor';
+
+export const DEFAULT_MONACO_CONFIG: MonacoEditorConstructionOptions = {
+ theme: 'vs',
+ minimap: {
+ enabled: false
+ },
+ cursorBlinking: 'solid',
+ overviewRulerBorder: false,
+ mouseWheelZoom: false,
+ lineNumbers: 'on',
+ roundedSelection: false,
+ automaticLayout: true,
+ fixedOverflowWidgets: true,
+ language: 'text',
+ fontSize: 14
+};
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-file-editor/store/form-file-editor.store.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-file-editor/store/form-file-editor.store.ts
new file mode 100644
index 000000000000..22dacea3f112
--- /dev/null
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-file-editor/store/form-file-editor.store.ts
@@ -0,0 +1,207 @@
+import { MonacoEditorConstructionOptions } from '@materia-ui/ngx-monaco-editor';
+import { tapResponse } from '@ngrx/operators';
+import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals';
+import { rxMethod } from '@ngrx/signals/rxjs-interop';
+import { pipe } from 'rxjs';
+
+import { computed, inject } from '@angular/core';
+
+import { switchMap, tap } from 'rxjs/operators';
+
+import { ComponentStatus, DotHttpErrorResponse } from '@dotcms/dotcms-models';
+
+import { UPLOAD_TYPE, UploadedFile } from '../../../models';
+import { DotFileFieldUploadService } from '../../../services/upload-file/upload-file.service';
+import { extractFileExtension, getInfoByLang } from '../../../utils/editor';
+import { DEFAULT_MONACO_CONFIG } from '../dot-form-file-editor.conts';
+
+type FileInfo = {
+ name: string;
+ content: string;
+ mimeType: string;
+ extension: string;
+ language: string;
+};
+
+export interface FormFileEditorState {
+ file: FileInfo;
+ allowFileNameEdit: boolean;
+ status: ComponentStatus;
+ error: string | null;
+ monacoOptions: MonacoEditorConstructionOptions;
+ uploadedFile: UploadedFile | null;
+ uploadType: UPLOAD_TYPE;
+ acceptedFiles: string[];
+}
+
+const initialState: FormFileEditorState = {
+ file: {
+ name: '',
+ content: '',
+ mimeType: 'plain/text',
+ extension: '.txt',
+ language: 'text'
+ },
+ allowFileNameEdit: false,
+ status: ComponentStatus.INIT,
+ error: null,
+ monacoOptions: DEFAULT_MONACO_CONFIG,
+ uploadedFile: null,
+ uploadType: 'dotasset',
+ acceptedFiles: []
+};
+
+export const FormFileEditorStore = signalStore(
+ withState(initialState),
+ withComputed((state) => ({
+ isUploading: computed(() => state.status() === ComponentStatus.LOADING),
+ isDone: computed(() => state.status() === ComponentStatus.LOADED && state.uploadedFile),
+ allowFiles: computed(() => state.acceptedFiles().join(', ')),
+ monacoConfig: computed(() => {
+ const monacoOptions = state.monacoOptions();
+ const { language } = state.file();
+
+ return {
+ ...monacoOptions,
+ language: language
+ };
+ })
+ })),
+ withMethods((store) => {
+ const uploadService = inject(DotFileFieldUploadService);
+
+ return {
+ /**
+ * Sets the file name and updates the file's metadata in the store.
+ *
+ * @param name - The new name of the file.
+ *
+ * This method performs the following actions:
+ * 1. Extracts the file extension from the provided name.
+ * 2. Retrieves file information based on the extracted extension.
+ * 3. Updates the store with the new file name and its associated metadata, including MIME type, extension, and language.
+ */
+ setFileName(name: string) {
+ const file = store.file();
+
+ const extension = extractFileExtension(name);
+ const info = getInfoByLang(extension);
+
+ patchState(store, {
+ file: {
+ ...file,
+ name,
+ mimeType: info.mimeType,
+ extension: info.extension,
+ language: info.lang
+ }
+ });
+ },
+ /**
+ * Initializes the file editor state with the provided options.
+ *
+ * @param params - The parameters for initializing the file editor.
+ * @param params.monacoOptions - Partial options for configuring the Monaco editor.
+ * @param params.allowFileNameEdit - Flag indicating if the file name can be edited.
+ * @param params.uploadedFile - The uploaded file information, or null if no file is uploaded.
+ * @param params.acceptedFiles - Array of accepted file types.
+ * @param params.uploadType - The type of upload being performed.
+ */
+ initLoad({
+ monacoOptions,
+ allowFileNameEdit,
+ uploadedFile,
+ acceptedFiles,
+ uploadType
+ }: {
+ monacoOptions: Partial;
+ allowFileNameEdit: boolean;
+ uploadedFile: UploadedFile | null;
+ acceptedFiles: string[];
+ uploadType: UPLOAD_TYPE;
+ }) {
+ const prevState = store.monacoOptions();
+
+ const state: Partial = {
+ monacoOptions: {
+ ...prevState,
+ ...monacoOptions
+ },
+ allowFileNameEdit,
+ acceptedFiles,
+ uploadType
+ };
+
+ if (uploadedFile) {
+ const { file, source } = uploadedFile;
+
+ const name = source === 'contentlet' ? file.title : file.fileName;
+ const extension = extractFileExtension(name);
+ const info = getInfoByLang(extension);
+
+ state.file = {
+ name,
+ content: file.content || '',
+ mimeType: info.mimeType,
+ extension: info.extension,
+ language: info.lang
+ };
+ }
+
+ patchState(store, state);
+ },
+ /**
+ * Uploads the file content to the server.
+ *
+ */
+ uploadFile: rxMethod<{
+ name: string;
+ content: string;
+ }>(
+ pipe(
+ tap(() => patchState(store, { status: ComponentStatus.LOADING })),
+ switchMap(({ name, content }) => {
+ const { mimeType: type } = store.file();
+ const uploadType = store.uploadType();
+ const acceptedFiles = store.acceptedFiles();
+
+ const file = new File([content], name, { type });
+
+ return uploadService
+ .uploadFile({
+ file,
+ uploadType,
+ acceptedFiles,
+ maxSize: null
+ })
+ .pipe(
+ tapResponse({
+ next: (uploadedFile) => {
+ patchState(store, {
+ uploadedFile,
+ status: ComponentStatus.LOADED
+ });
+ },
+ error: (error: DotHttpErrorResponse) => {
+ let errorMessage = error?.message || '';
+
+ if (error instanceof Error) {
+ if (errorMessage === 'Invalid file type') {
+ errorMessage =
+ 'dot.file.field.error.type.file.not.supported.message';
+ }
+ }
+
+ patchState(store, {
+ error: errorMessage,
+ status: ComponentStatus.ERROR
+ });
+ }
+ })
+ );
+ })
+ )
+ )
+ };
+ })
+);
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/dot-form-import-url.component.html b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/dot-form-import-url.component.html
index d72d4c9817e4..1a996fa390d7 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/dot-form-import-url.component.html
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/dot-form-import-url.component.html
@@ -19,7 +19,7 @@
} @else {
}
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/dot-form-import-url.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/dot-form-import-url.component.spec.ts
index c5d122b79d38..278f73bdad29 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/dot-form-import-url.component.spec.ts
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/dot-form-import-url.component.spec.ts
@@ -8,6 +8,7 @@ import { ReactiveFormsModule } from '@angular/forms';
import { DialogService, DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { DotMessageService } from '@dotcms/data-access';
+import { ComponentStatus } from '@dotcms/dotcms-models';
import { DotFormImportUrlComponent } from './dot-form-import-url.component';
import { FormImportUrlStore } from './store/form-import-url.store';
@@ -64,7 +65,7 @@ describe('DotFormImportUrlComponent', () => {
patchState(store, {
file: mockPreviewFile,
- status: 'done'
+ status: ComponentStatus.LOADED
});
spectator.flushEffects();
@@ -78,7 +79,7 @@ describe('DotFormImportUrlComponent', () => {
const enableSpy = jest.spyOn(spectator.component.form, 'enable');
patchState(store, {
- status: 'uploading'
+ status: ComponentStatus.LOADING
});
spectator.flushEffects();
@@ -86,7 +87,7 @@ describe('DotFormImportUrlComponent', () => {
expect(disableSpy).toHaveBeenCalled();
patchState(store, {
- status: 'done'
+ status: ComponentStatus.LOADED
});
spectator.flushEffects();
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/dot-form-import-url.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/dot-form-import-url.component.ts
index 1e38d552e515..a97121b6daac 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/dot-form-import-url.component.ts
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/dot-form-import-url.component.ts
@@ -1,4 +1,11 @@
-import { ChangeDetectionStrategy, Component, effect, inject, OnInit } from '@angular/core';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ effect,
+ inject,
+ OnInit,
+ untracked
+} from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonModule } from 'primeng/button';
@@ -35,7 +42,7 @@ export class DotFormImportUrlComponent implements OnInit {
);
#abortController: AbortController | null = null;
- readonly form = this.#formBuilder.group({
+ readonly form = this.#formBuilder.nonNullable.group({
url: ['', [Validators.required, DotValidators.url]]
});
@@ -49,9 +56,11 @@ export class DotFormImportUrlComponent implements OnInit {
const file = this.store.file();
const isDone = this.store.isDone();
- if (file && isDone) {
- this.#dialogRef.close(file);
- }
+ untracked(() => {
+ if (isDone) {
+ this.#dialogRef.close(file);
+ }
+ });
},
{
allowSignalWrites: true
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/store/form-import-url.store.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/store/form-import-url.store.spec.ts
index 821f0ded9f53..64be5196b51b 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/store/form-import-url.store.spec.ts
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/store/form-import-url.store.spec.ts
@@ -3,6 +3,8 @@ import { of, throwError } from 'rxjs';
import { TestBed } from '@angular/core/testing';
+import { ComponentStatus } from '@dotcms/dotcms-models';
+
import { NEW_FILE_MOCK } from './../../../../../utils/mocks';
import { FormImportUrlStore, FormImportUrlState } from './form-import-url.store';
@@ -29,7 +31,7 @@ describe('FormImportUrlStore', () => {
it('should initialize with the correct state', () => {
expect(store.file()).toBeNull();
- expect(store.status()).toBe('init');
+ expect(store.status()).toBe(ComponentStatus.INIT);
expect(store.error()).toBeNull();
expect(store.uploadType()).toBe('temp');
expect(store.acceptedFiles()).toEqual([]);
@@ -61,9 +63,9 @@ describe('FormImportUrlStore', () => {
store.uploadFileByUrl({ fileUrl, abortSignal: abortController.signal });
- expect(store.file().file).toEqual(mockContentlet);
- expect(store.file().source).toEqual('contentlet');
- expect(store.status()).toBe('done');
+ expect(store.file()?.file).toEqual(mockContentlet);
+ expect(store.file()?.source).toEqual('contentlet');
+ expect(store.status()).toBe(ComponentStatus.LOADED);
});
it('should handle upload file by URL error', () => {
@@ -77,7 +79,7 @@ describe('FormImportUrlStore', () => {
expect(store.error()).toBe(
'dot.file.field.import.from.url.error.file.not.supported.message'
);
- expect(store.status()).toBe('error');
+ expect(store.status()).toBe(ComponentStatus.ERROR);
});
});
});
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/store/form-import-url.store.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/store/form-import-url.store.ts
index 6677b0b6c897..de1dad6995a7 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/store/form-import-url.store.ts
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/components/dot-form-import-url/store/form-import-url.store.ts
@@ -7,14 +7,14 @@ import { computed, inject } from '@angular/core';
import { switchMap, tap } from 'rxjs/operators';
-import { DotHttpErrorResponse } from '@dotcms/dotcms-models';
+import { ComponentStatus, DotHttpErrorResponse } from '@dotcms/dotcms-models';
import { UploadedFile, UPLOAD_TYPE } from '../../../models';
import { DotFileFieldUploadService } from '../../../services/upload-file/upload-file.service';
export interface FormImportUrlState {
file: UploadedFile | null;
- status: 'init' | 'uploading' | 'done' | 'error';
+ status: ComponentStatus;
error: string | null;
uploadType: UPLOAD_TYPE;
acceptedFiles: string[];
@@ -22,7 +22,7 @@ export interface FormImportUrlState {
const initialState: FormImportUrlState = {
file: null,
- status: 'init',
+ status: ComponentStatus.INIT,
error: null,
uploadType: 'temp',
acceptedFiles: []
@@ -31,8 +31,8 @@ const initialState: FormImportUrlState = {
export const FormImportUrlStore = signalStore(
withState(initialState),
withComputed((state) => ({
- isLoading: computed(() => state.status() === 'uploading'),
- isDone: computed(() => state.status() === 'done'),
+ isLoading: computed(() => state.status() === ComponentStatus.LOADING),
+ isDone: computed(() => state.status() === ComponentStatus.LOADED && state.file),
allowFiles: computed(() => state.acceptedFiles().join(', '))
})),
withMethods((store, uploadService = inject(DotFileFieldUploadService)) => ({
@@ -45,7 +45,7 @@ export const FormImportUrlStore = signalStore(
abortSignal: AbortSignal;
}>(
pipe(
- tap(() => patchState(store, { status: 'uploading' })),
+ tap(() => patchState(store, { status: ComponentStatus.LOADING })),
switchMap(({ fileUrl, abortSignal }) => {
return uploadService
.uploadFile({
@@ -58,7 +58,7 @@ export const FormImportUrlStore = signalStore(
.pipe(
tapResponse({
next: (file) => {
- patchState(store, { file, status: 'done' });
+ patchState(store, { file, status: ComponentStatus.LOADED });
},
error: (error: DotHttpErrorResponse) => {
let errorMessage = error?.message || '';
@@ -72,7 +72,7 @@ export const FormImportUrlStore = signalStore(
patchState(store, {
error: errorMessage,
- status: 'error'
+ status: ComponentStatus.ERROR
});
}
})
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 39c6954bc2a0..44b1be904025 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
@@ -51,6 +51,7 @@
}
@if (store.allowCreateFile()) {
}
@case ('preview') {
- @if (store.uploadedFile()) {
+ @let uploadedFile = store.uploadedFile();
+ @if (uploadedFile) {
+ [previewFile]="uploadedFile" />
}
}
}
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 e7a3bdab3743..2782ccb6bc35 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
@@ -32,6 +32,7 @@ import {
import { DotFileFieldPreviewComponent } from './components/dot-file-field-preview/dot-file-field-preview.component';
import { DotFileFieldUiMessageComponent } from './components/dot-file-field-ui-message/dot-file-field-ui-message.component';
+import { DotFormFileEditorComponent } from './components/dot-form-file-editor/dot-form-file-editor.component';
import { DotFormImportUrlComponent } from './components/dot-form-import-url/dot-form-import-url.component';
import { INPUT_TYPES, UploadedFile } from './models';
import { DotFileFieldUploadService } from './services/upload-file/upload-file.service';
@@ -132,21 +133,19 @@ export class DotEditContentFileFieldComponent implements ControlValueAccessor, O
return this.#dotMessageService.get('dot.file.field.action.generate.with.tooltip');
}
- return null;
+ return '';
});
- private onChange: (value: string) => void;
- private onTouched: () => void;
+ private onChange: ((value: string) => void) | null = null;
+ private onTouched: (() => void) | null = null;
constructor() {
effect(() => {
- if (!this.onChange && !this.onTouched) {
- return;
+ if (this.onChange && this.onTouched) {
+ const value = this.store.value();
+ this.onChange(value);
+ this.onTouched();
}
-
- const value = this.store.value();
- this.onChange(value);
- this.onTouched();
});
}
@@ -234,7 +233,11 @@ export class DotEditContentFileFieldComponent implements ControlValueAccessor, O
*
* @return {void}
*/
- fileSelected(files: FileList) {
+ fileSelected(files: FileList | null) {
+ if (!files || files.length === 0) {
+ return;
+ }
+
const file = files[0];
if (!file) {
@@ -346,6 +349,46 @@ export class DotEditContentFileFieldComponent implements ControlValueAccessor, O
});
}
+ /**
+ * Opens the file editor dialog with specific configurations and handles the file upload process.
+ *
+ * This method performs the following actions:
+ * - Retrieves the header message for the dialog.
+ * - Opens the `DotFormFileEditorComponent` dialog with various options such as header, appendTo, closeOnEscape, draggable, keepInViewport, maskStyleClass, resizable, modal, width, and style.
+ * - Passes data to the dialog, including the uploaded file and a flag to allow file name editing.
+ * - Subscribes to the dialog's onClose event to handle the uploaded file and update the store with the preview file.
+ *
+ */
+ showFileEditorDialog() {
+ const header = this.#dotMessageService.get('dot.file.field.dialog.create.new.file.header');
+
+ this.#dialogRef = this.#dialogService.open(DotFormFileEditorComponent, {
+ header,
+ appendTo: 'body',
+ closeOnEscape: false,
+ draggable: false,
+ keepInViewport: false,
+ maskStyleClass: 'p-dialog-mask-transparent-ai',
+ resizable: false,
+ modal: true,
+ width: '90%',
+ style: { 'max-width': '1040px' },
+ data: {
+ uploadedFile: this.store.uploadedFile(),
+ allowFileNameEdit: true
+ }
+ });
+
+ this.#dialogRef.onClose
+ .pipe(
+ filter((file) => !!file),
+ takeUntilDestroyed(this.#destroyRef)
+ )
+ .subscribe((file) => {
+ this.store.setPreviewFile(file);
+ });
+ }
+
/**
* Cleanup method.
*
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.const.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.const.ts
index 796aa5997f6f..2cd8dcccd076 100644
--- a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.const.ts
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/dot-edit-content-file-field.const.ts
@@ -6,7 +6,7 @@ type Actions = {
allowCreateFile: boolean;
allowGenerateImg: boolean;
acceptedFiles: string[];
- maxFileSize: number;
+ maxFileSize: number | null;
};
type ConfigActions = Record;
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
index 62c65593b450..5ec332eecd08 100644
--- 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
@@ -76,7 +76,7 @@ export const FileFieldStore = signalStore(
* @param initState
*/
initLoad: (initState: {
- inputType: FileFieldState['inputType'];
+ inputType: INPUT_TYPES;
fieldVariable: FileFieldState['fieldVariable'];
isAIPluginInstalled?: boolean;
}) => {
diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/editor.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/editor.ts
new file mode 100644
index 000000000000..b1e189198080
--- /dev/null
+++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-file-field/utils/editor.ts
@@ -0,0 +1,53 @@
+/**
+ * Extracts the file extension from a given file name.
+ *
+ * @param fileName - The name of the file from which to extract the extension.
+ * @returns The file extension if present, otherwise an empty string.
+ */
+export function extractFileExtension(fileName: string): string {
+ const includesDot = fileName.includes('.');
+
+ if (!includesDot) {
+ return '';
+ }
+
+ return fileName.split('.').pop() || '';
+}
+
+/**
+ * Retrieves language information based on the provided file extension.
+ *
+ * @param extension - The file extension to get the language information for.
+ * @returns An object containing the language id, MIME type, and extension.
+ *
+ * @example
+ * ```typescript
+ * const info = getInfoByLang('vtl');
+ * console.log(info);
+ * // Output: { lang: 'html', mimeType: 'text/x-velocity', extension: '.vtl' }
+ * ```
+ *
+ * @remarks
+ * If the extension is 'vtl', it returns a predefined set of values.
+ * Otherwise, it searches through the Monaco Editor languages to find a match.
+ * If no match is found, it defaults to 'text' for the language id, 'text/plain' for the MIME type, and '.txt' for the extension.
+ */
+export function getInfoByLang(extension: string) {
+ if (extension === 'vtl') {
+ return {
+ lang: 'html',
+ mimeType: 'text/x-velocity',
+ extension: '.vtl'
+ };
+ }
+
+ const language = monaco.languages
+ .getLanguages()
+ .find((language) => language.extensions?.includes(`.${extension}`));
+
+ return {
+ lang: language?.id || 'text',
+ mimeType: language?.mimetypes?.[0] || 'text/plain',
+ extension: language?.extensions?.[0] || '.txt'
+ };
+}
diff --git a/core-web/libs/edit-content/tsconfig.json b/core-web/libs/edit-content/tsconfig.json
index 56e37bfd9172..2c3af383b4f6 100644
--- a/core-web/libs/edit-content/tsconfig.json
+++ b/core-web/libs/edit-content/tsconfig.json
@@ -5,7 +5,8 @@
"forceConsistentCasingInFileNames": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
- "noFallthroughCasesInSwitch": true
+ "noFallthroughCasesInSwitch": true,
+ "strict": false
},
"files": [],
"include": [],
diff --git a/core-web/libs/ui/src/lib/components/dot-drop-zone/dot-drop-zone.component.ts b/core-web/libs/ui/src/lib/components/dot-drop-zone/dot-drop-zone.component.ts
index 5292f81ac39d..53833d171948 100644
--- a/core-web/libs/ui/src/lib/components/dot-drop-zone/dot-drop-zone.component.ts
+++ b/core-web/libs/ui/src/lib/components/dot-drop-zone/dot-drop-zone.component.ts
@@ -44,7 +44,7 @@ export class DotDropZoneComponent {
* Max file size in bytes.
* See Docs: https://www.dotcms.com/docs/latest/binary-field#FieldVariables
*/
- @Input() maxFileSize: number;
+ @Input() maxFileSize: number | null = null;
@Input() set accept(types: string[]) {
this._accept = types