-
Notifications
You must be signed in to change notification settings - Fork 468
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(block-editor): add import from url feature (#30242)
### Parent Issue #29874 ### Proposed Changes * Create `dot-form-import-url` component * Create `FormImportUrlStore` store using signals * Implement the "Import from URL" for Image and File fields ### Checklist - [x] Tests - [x] Translations - [x] Security Implications Contemplated (add notes if applicable) ### Screenshots https://github.com/user-attachments/assets/5362fd77-2d5e-4062-b0af-c8e89f79e271
- Loading branch information
Showing
15 changed files
with
489 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
...edit-content-file-field/components/dot-form-import-url/dot-form-import-url.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
<form (ngSubmit)="onSubmit()" [formGroup]="form" class="url-mode__form" data-testId="form"> | ||
<div class="url-mode__input-container"> | ||
<label for="url-input">{{ 'dot.file.field.action.import.from.url' | dm }}</label> | ||
<input | ||
id="url-input" | ||
type="text" | ||
autocomplete="off" | ||
formControlName="url" | ||
pInputText | ||
placeholder="https://www.dotcms.com/image.png" | ||
aria-label="URL input field" | ||
data-testId="url-input" /> | ||
<div class="error-messsage__container"> | ||
@let error = store.error(); | ||
@if (error) { | ||
<small class="p-invalid" data-testId="error-msg"> | ||
{{ error | dm }} | ||
</small> | ||
} @else { | ||
<dot-field-validation-message | ||
[message]="'dot.file.field.action.import.from.url.error.message' | dm" | ||
[field]="form.get('url')" | ||
data-testId="error-message" /> | ||
} | ||
</div> | ||
</div> | ||
<div class="url-mode__actions"> | ||
<p-button | ||
(click)="cancelUpload()" | ||
[label]="'dot.common.cancel' | dm" | ||
styleClass="p-button-outlined" | ||
type="button" | ||
aria-label="Cancel button" | ||
data-testId="cancel-button" /> | ||
<div> | ||
@if (store.isLoading()) { | ||
<p-button | ||
[icon]="'pi pi-spin pi-spinner'" | ||
type="button" | ||
aria-label="Loading button" | ||
data-testId="loading-button" /> | ||
} @else { | ||
<p-button | ||
[label]="'dot.common.import' | dm" | ||
[icon]="'pi pi-download'" | ||
type="submit" | ||
aria-label="Import button" | ||
data-testId="import-button" /> | ||
} | ||
</div> | ||
</div> | ||
</form> |
37 changes: 37 additions & 0 deletions
37
...edit-content-file-field/components/dot-form-import-url/dot-form-import-url.component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
@use "variables" as *; | ||
|
||
:host ::ng-deep { | ||
display: block; | ||
width: 32rem; | ||
|
||
.p-button { | ||
width: 100%; | ||
} | ||
|
||
.error-messsage__container { | ||
min-height: $spacing-4; | ||
} | ||
} | ||
|
||
.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; | ||
} |
101 changes: 101 additions & 0 deletions
101
...t-edit-content-file-field/components/dot-form-import-url/dot-form-import-url.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { ChangeDetectionStrategy, Component, effect, inject, OnInit } from '@angular/core'; | ||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; | ||
|
||
import { ButtonModule } from 'primeng/button'; | ||
import { DynamicDialogRef, DynamicDialogConfig } from 'primeng/dynamicdialog'; | ||
import { InputTextModule } from 'primeng/inputtext'; | ||
|
||
import { DotMessagePipe, DotFieldValidationMessageComponent, DotValidators } from '@dotcms/ui'; | ||
|
||
import { FormImportUrlStore } from './store/form-import-url.store'; | ||
|
||
import { INPUT_TYPE } from '../../../dot-edit-content-text-field/utils'; | ||
|
||
@Component({ | ||
selector: 'dot-form-import-url', | ||
standalone: true, | ||
imports: [ | ||
DotMessagePipe, | ||
ReactiveFormsModule, | ||
DotFieldValidationMessageComponent, | ||
ButtonModule, | ||
InputTextModule | ||
], | ||
templateUrl: './dot-form-import-url.component.html', | ||
styleUrls: ['./dot-form-import-url.component.scss'], | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
providers: [FormImportUrlStore] | ||
}) | ||
export class DotFormImportUrlComponent implements OnInit { | ||
readonly store = inject(FormImportUrlStore); | ||
readonly #formBuilder = inject(FormBuilder); | ||
readonly #dialogRef = inject(DynamicDialogRef); | ||
readonly #dialogConfig = inject(DynamicDialogConfig<{ inputType: INPUT_TYPE }>); | ||
|
||
readonly form = this.#formBuilder.group({ | ||
url: ['', [Validators.required, DotValidators.url]] | ||
}); | ||
|
||
/** | ||
* Listens to the `file` and `isDone` signals and closes the dialog once both are truthy. | ||
* The `file` value is passed as the dialog result. | ||
*/ | ||
constructor() { | ||
effect( | ||
() => { | ||
const file = this.store.file(); | ||
const isDone = this.store.isDone(); | ||
|
||
if (file && isDone) { | ||
this.#dialogRef.close(file); | ||
} | ||
}, | ||
{ | ||
allowSignalWrites: true | ||
} | ||
); | ||
|
||
effect(() => { | ||
const isLoading = this.store.isLoading(); | ||
if (isLoading) { | ||
this.form.disable(); | ||
} else { | ||
this.form.enable(); | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Initializes the component by setting the upload type based on the input type | ||
* of the parent dialog. | ||
* | ||
* If the input type is 'Binary', the upload type is set to 'temp', otherwise it's set to 'dotasset'. | ||
*/ | ||
ngOnInit(): void { | ||
const uploadType = this.#dialogConfig?.data?.inputType === 'Binary' ? 'temp' : 'dotasset'; | ||
this.store.setUploadType(uploadType); | ||
} | ||
|
||
/** | ||
* Submits the form, if it's valid, by calling the `uploadFileByUrl` method of the store. | ||
* | ||
* @return {void} | ||
*/ | ||
onSubmit(): void { | ||
if (this.form.invalid) { | ||
return; | ||
} | ||
|
||
const { url } = this.form.getRawValue(); | ||
this.store.uploadFileByUrl(url); | ||
} | ||
|
||
/** | ||
* Cancels the upload and closes the dialog. | ||
* | ||
* @return {void} | ||
*/ | ||
cancelUpload(): void { | ||
this.#dialogRef.close(); | ||
} | ||
} |
64 changes: 64 additions & 0 deletions
64
...dot-edit-content-file-field/components/dot-form-import-url/store/form-import-url.store.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
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 { 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'; | ||
error: string | null; | ||
uploadType: UPLOAD_TYPE; | ||
} | ||
|
||
const initialState: FormImportUrlState = { | ||
file: null, | ||
status: 'init', | ||
error: null, | ||
uploadType: 'temp' | ||
}; | ||
|
||
export const FormImportUrlStore = signalStore( | ||
withState(initialState), | ||
withComputed((state) => ({ | ||
isLoading: computed(() => state.status() === 'uploading'), | ||
isDone: computed(() => state.status() === 'done') | ||
})), | ||
withMethods((store, uploadService = inject(DotFileFieldUploadService)) => ({ | ||
/** | ||
* uploadFileByUrl - Uploads a file using its URL. | ||
* @param {string} fileUrl - The URL of the file to be uploaded. | ||
*/ | ||
uploadFileByUrl: rxMethod<string>( | ||
pipe( | ||
tap(() => patchState(store, { status: 'uploading' })), | ||
switchMap((fileUrl) => { | ||
return uploadService | ||
.uploadFile({ | ||
file: fileUrl, | ||
uploadType: store.uploadType() | ||
}) | ||
.pipe( | ||
tapResponse({ | ||
next: (file) => patchState(store, { file, status: 'done' }), | ||
error: console.error | ||
}) | ||
); | ||
}) | ||
) | ||
), | ||
/** | ||
* Set the upload type (contentlet or temp) for the file. | ||
* @param uploadType the type of upload to perform | ||
*/ | ||
setUploadType: (uploadType: FormImportUrlState['uploadType']) => { | ||
patchState(store, { uploadType }); | ||
} | ||
})) | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.