From 487662dc9366fa1a57e2fd7e6326b537cced7dfb Mon Sep 17 00:00:00 2001 From: Nicolas Molina Date: Tue, 22 Oct 2024 19:58:23 -0400 Subject: [PATCH] chore(edit-content): upload file #29875 --- .../utils/editor.ts | 29 ++++ .../dot-form-file-editor.component.html | 8 +- .../dot-form-file-editor.component.ts | 60 +++++-- .../dot-form-file-editor.conts.ts | 2 - .../store/form-file-editor.store.ts | 157 ++++++++++++++---- .../dot-form-import-url.component.html | 2 +- .../dot-form-import-url.component.ts | 17 +- .../store/form-import-url.store.spec.ts | 4 +- .../store/form-import-url.store.ts | 16 +- ...dot-edit-content-file-field.component.html | 5 +- .../dot-edit-content-file-field.component.ts | 35 ++-- .../dot-edit-content-file-field.const.ts | 2 +- 12 files changed, 260 insertions(+), 77 deletions(-) create mode 100644 core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/utils/editor.ts diff --git a/core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/utils/editor.ts b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/utils/editor.ts new file mode 100644 index 000000000000..708fd1b66f51 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-edit-content-binary-field/utils/editor.ts @@ -0,0 +1,29 @@ +export function extractFileExtension(fileName: string): string { + const includesDot = fileName.includes('.'); + + if (!includesDot) { + return ''; + } + + return fileName.split('.').pop() || ''; +} + +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/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 index 5b60054d3540..731f55de5dc2 100644 --- 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 @@ -30,15 +30,15 @@ data-testId="code-editor" formControlName="content" /> - @let mimeType = store.file()?.mimeType; -
+ @let file = store.file(); +
- Mime Type: {{ mimeType }} + Mime Type: {{ file.mimeType }}
; uploadedFile: UploadedFile | null; -} +}; @Component({ selector: 'dot-form-file-editor', @@ -50,7 +53,6 @@ type DialogProps = { export class DotFormFileEditorComponent implements OnInit { readonly store = inject(FormFileEditorStore); readonly #formBuilder = inject(FormBuilder); - readonly #dotMessageService = inject(DotMessageService); readonly #dialogRef = inject(DynamicDialogRef); readonly #dialogConfig = inject(DynamicDialogConfig); @@ -59,14 +61,9 @@ export class DotFormFileEditorComponent implements OnInit { content: [''] }); - - tempFileUploaded = output(); - cancel = output(); - $editorRef = viewChild.required(MonacoEditorComponent); constructor() { - effect(() => { const isUploading = this.store.isUploading(); @@ -76,6 +73,33 @@ export class DotFormFileEditorComponent implements OnInit { 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); + }); } ngOnInit(): void { @@ -92,7 +116,10 @@ export class DotFormFileEditorComponent implements OnInit { this.store.initLoad({ monacoOptions: userMonacoOptions || {}, - allowFileNameEdit: allowFileNameEdit || true + allowFileNameEdit: allowFileNameEdit || true, + uploadedFile, + acceptedFiles: [], + uploadType: 'dotasset' }); } @@ -104,15 +131,16 @@ export class DotFormFileEditorComponent implements OnInit { return; } - this.store.uploadFile(); + const values = this.form.getRawValue(); + this.store.uploadFile(values); } get nameField() { - return this.form.get('name'); + return this.form.controls.name; } get contentField() { - return this.form.get('content'); + return this.form.controls.content; } #disableEditor() { @@ -133,4 +161,8 @@ export class DotFormFileEditorComponent implements OnInit { content: file.content }); } + + cancelUpload(): void { + this.#dialogRef.close(); + } } 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 index 7ed58154fabd..db6cdf79463b 100644 --- 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 @@ -1,7 +1,5 @@ import { MonacoEditorConstructionOptions } from '@materia-ui/ngx-monaco-editor'; -export const DEFAULT_FILE_TYPE = 'text'; - export const DEFAULT_MONACO_CONFIG: MonacoEditorConstructionOptions = { theme: 'vs', minimap: { 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 index 7cf46da6fa85..6ad8952732b9 100644 --- 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 @@ -1,44 +1,68 @@ 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 } from '@angular/core'; +import { computed, inject } from '@angular/core'; -import { ComponentStatus } from '@dotcms/dotcms-models'; +import { switchMap, tap } from 'rxjs/operators'; -import { DEFAULT_FILE_TYPE, DEFAULT_MONACO_CONFIG } from '../dot-form-file-editor.conts'; +import { ComponentStatus, DotHttpErrorResponse } from '@dotcms/dotcms-models'; + +import { + extractFileExtension, + getInfoByLang +} from '../../../../dot-edit-content-binary-field/utils/editor'; +import { UPLOAD_TYPE, UploadedFile } from '../../../models'; +import { DotFileFieldUploadService } from '../../../services/upload-file/upload-file.service'; +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 | null; + file: FileInfo; allowFileNameEdit: boolean; status: ComponentStatus; error: string | null; - languageType: string; monacoOptions: MonacoEditorConstructionOptions; + uploadedFile: UploadedFile | null; + uploadType: UPLOAD_TYPE; + acceptedFiles: string[]; } const initialState: FormFileEditorState = { - file: null, + file: { + name: '', + content: '', + mimeType: 'plain/text', + extension: '.txt', + language: 'text' + }, allowFileNameEdit: false, status: ComponentStatus.INIT, error: null, - languageType: DEFAULT_FILE_TYPE, - monacoOptions: DEFAULT_MONACO_CONFIG + 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.languageType(); + const { language } = state.file(); return { ...monacoOptions, @@ -47,39 +71,116 @@ export const FormFileEditorStore = signalStore( }) })), withMethods((store) => { + const uploadService = inject(DotFileFieldUploadService); + return { - setFile(file: FileInfo) { - patchState(store, { file }); + 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 + } + }); }, - initLoad({monacoOptions, allowFileNameEdit}:{ - monacoOptions: Partial, - allowFileNameEdit: boolean + initLoad({ + monacoOptions, + allowFileNameEdit, + uploadedFile, + acceptedFiles, + uploadType + }: { + monacoOptions: Partial; + allowFileNameEdit: boolean; + uploadedFile: UploadedFile | null; + acceptedFiles: string[]; + uploadType: UPLOAD_TYPE; }) { const prevState = store.monacoOptions(); - patchState(store, { + const state: Partial = { monacoOptions: { ...prevState, ...monacoOptions }, - allowFileNameEdit - }); - }, - uploadFile(): void { - const fileInfo = store.file(); + allowFileNameEdit, + acceptedFiles, + uploadType + }; + + if (uploadedFile) { + const { file, source } = uploadedFile; - if (!fileInfo) { - return; + 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 + }; } - const file = new File([fileInfo.content], fileInfo.name, { - type: fileInfo.mimeType - }); + patchState(store, state); + }, + 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 || ''; - console.log(file); + if (error instanceof Error) { + if (errorMessage === 'Invalid file type') { + errorMessage = + 'dot.file.field.error.type.file.not.supported.message'; + } + } - // implementation - } + 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.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 058c462cf77d..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'; @@ -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..17e6d5aa41b5 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 @@ -61,8 +61,8 @@ describe('FormImportUrlStore', () => { store.uploadFileByUrl({ fileUrl, abortSignal: abortController.signal }); - expect(store.file().file).toEqual(mockContentlet); - expect(store.file().source).toEqual('contentlet'); + expect(store.file()?.file).toEqual(mockContentlet); + expect(store.file()?.source).toEqual('contentlet'); expect(store.status()).toBe('done'); }); 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 57e505e1375f..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 @@ -93,10 +93,11 @@ } @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 36a40df9220a..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 @@ -136,18 +136,16 @@ export class DotEditContentFileFieldComponent implements ControlValueAccessor, O 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(); }); } @@ -351,6 +349,16 @@ 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'); @@ -371,9 +379,14 @@ export class DotEditContentFileFieldComponent implements ControlValueAccessor, O } }); - this.#dialogRef.onClose.pipe(takeUntilDestroyed(this.#destroyRef)).subscribe((file) => { - console.log('file', file); - }); + this.#dialogRef.onClose + .pipe( + filter((file) => !!file), + takeUntilDestroyed(this.#destroyRef) + ) + .subscribe((file) => { + this.store.setPreviewFile(file); + }); } /** 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;