From 5804d8cc9f015dc25540b1999bc66be5beb7a772 Mon Sep 17 00:00:00 2001 From: Florian THIERRY Date: Thu, 5 Sep 2024 17:28:30 +0200 Subject: [PATCH] Extract publication edition into a separated component. --- frontend/src/app/app.routes.ts | 2 +- .../publication-edition.component.html | 87 ++++++++++ .../publication-edition.component.scss | 14 +- .../publication-edition.component.ts | 155 ++++++++++++++++++ .../publication-edition.service.ts | 27 ++- .../publication-edition.component.html | 97 ----------- .../publication-edition.component.ts | 149 ----------------- .../publication-edition.routes.ts | 7 - .../publication-update.component.html | 13 ++ .../publication-update.component.scss | 7 + .../publication-update.component.ts | 97 +++++++++++ .../publication-update.routes.ts | 7 + 12 files changed, 394 insertions(+), 268 deletions(-) create mode 100644 frontend/src/app/components/publication-edition/publication-edition.component.html rename frontend/src/app/{pages => components}/publication-edition/publication-edition.component.scss (95%) create mode 100644 frontend/src/app/components/publication-edition/publication-edition.component.ts rename frontend/src/app/{pages => components}/publication-edition/publication-edition.service.ts (93%) delete mode 100644 frontend/src/app/pages/publication-edition/publication-edition.component.html delete mode 100644 frontend/src/app/pages/publication-edition/publication-edition.component.ts delete mode 100644 frontend/src/app/pages/publication-edition/publication-edition.routes.ts create mode 100644 frontend/src/app/pages/publication-edition/publication-update.component.html create mode 100644 frontend/src/app/pages/publication-edition/publication-update.component.scss create mode 100644 frontend/src/app/pages/publication-edition/publication-update.component.ts create mode 100644 frontend/src/app/pages/publication-edition/publication-update.routes.ts diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index e620ba4..eef9dcf 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -19,7 +19,7 @@ export const routes: Routes = [ }, { path: 'publications/:publicationId/edit', - loadChildren: () => import('./pages/publication-edition/publication-edition.routes').then(module => module.ROUTES) + loadChildren: () => import('./pages/publication-edition/publication-update.routes').then(module => module.ROUTES) }, { path: 'publications', diff --git a/frontend/src/app/components/publication-edition/publication-edition.component.html b/frontend/src/app/components/publication-edition/publication-edition.component.html new file mode 100644 index 0000000..990332a --- /dev/null +++ b/frontend/src/app/components/publication-edition/publication-edition.component.html @@ -0,0 +1,87 @@ +
+
+

Modification de l'article {{ publication.title }}

+
+ + + +
+
+
+ + Title + + + + Description + + +
+ +
+ +
+
+ +
+ + + + + + + +
+ + Content + + +
+
+ + +
+ @if ((isPreviewing$ | async) === true) { +
+

Preview is loading...

+ +
+ } @else { + +
+

{{ publication.title }}

+

{{ publication.description }}

+
+
+ } +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/frontend/src/app/pages/publication-edition/publication-edition.component.scss b/frontend/src/app/components/publication-edition/publication-edition.component.scss similarity index 95% rename from frontend/src/app/pages/publication-edition/publication-edition.component.scss rename to frontend/src/app/components/publication-edition/publication-edition.component.scss index 6256654..f35b73e 100644 --- a/frontend/src/app/pages/publication-edition/publication-edition.component.scss +++ b/frontend/src/app/components/publication-edition/publication-edition.component.scss @@ -8,7 +8,7 @@ margin: 1em; max-width: 80em; width: 90%; - border-radius: .5em; + border-radius: .5em; box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12); & > header { @@ -113,10 +113,6 @@ } } -:host ::ng-deep .test circle { - stroke: white; -} - button, a.button { padding: .8em 1.2em; border-radius: 10em; @@ -151,9 +147,15 @@ button, a.button { flex-direction: column; max-height: 80vh; overflow-y: auto; - align-items: center; + + .preview-loading { + display: flex; + flex-direction: column; + align-items: center; + } .illustration { + flex: 1; height: 12em; object-fit: cover; transition: height .2s ease-in-out; diff --git a/frontend/src/app/components/publication-edition/publication-edition.component.ts b/frontend/src/app/components/publication-edition/publication-edition.component.ts new file mode 100644 index 0000000..17cfdc2 --- /dev/null +++ b/frontend/src/app/components/publication-edition/publication-edition.component.ts @@ -0,0 +1,155 @@ +import { CommonModule, Location } from "@angular/common"; +import { Component, EventEmitter, inject, Input, OnChanges, OnDestroy, Output } from "@angular/core"; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; +import { MatDialogModule } from "@angular/material/dialog"; +import { MatIconModule } from "@angular/material/icon"; +import { MatInputModule } from "@angular/material/input"; +import { MatProgressSpinnerModule } from "@angular/material/progress-spinner"; +import { MatTabsModule } from "@angular/material/tabs"; +import { MatTooltipModule } from "@angular/material/tooltip"; +import { map, Observable, of, Subscription } from "rxjs"; +import { Publication } from "../../core/rest-services/publications/model/publication"; +import { PictureSelectionDialog } from "../../pages/publication-edition/picture-selection-dialog/picture-selection-dialog.component"; +import { SubmitButtonComponent } from "../submit-button/submit-button.component"; +import { PublicationEditionService } from "./publication-edition.service"; + +@Component({ + selector: 'app-publication-edition', + standalone: true, + templateUrl: './publication-edition.component.html', + styleUrl: './publication-edition.component.scss', + imports: [ + CommonModule, + MatDialogModule, + MatIconModule, + MatInputModule, + MatProgressSpinnerModule, + MatTabsModule, + MatTooltipModule, + PictureSelectionDialog, + ReactiveFormsModule, + SubmitButtonComponent + ], + providers: [PublicationEditionService] +}) +export class PublicationEditionComponent implements OnChanges, OnDestroy { + @Input() + publication!: Publication; + @Input() + isSaving$: Observable = of(false); + @Output() + publicationSave = new EventEmitter(); + + private readonly formBuilder = inject(FormBuilder); + private readonly location = inject(Location); + private readonly publicationEditionService = inject(PublicationEditionService); + private publicationInEdition!: Publication; + private subscriptions: Subscription[] = []; + + publicationEditionForm: FormGroup = this.formBuilder.group({ + title: new FormControl('', [Validators.required]), + description: new FormControl('', [Validators.required]), + text: new FormControl('', [Validators.required]), + illustrationId: new FormControl('', [Validators.required]), + categoryId: new FormControl('', [Validators.required]) + }); + + get isLoading$(): Observable { + return this.publicationEditionService.isLoading$; + } + + get isPreviewing$(): Observable { + return this.publicationEditionService.isPreviewing$; + } + + ngOnChanges(): void { + this.ngOnDestroy(); + + this.publicationInEdition = this.publication; + this.publicationEditionService.init(this.publicationInEdition); + + ['title', 'description', 'text'].forEach(fieldName => { + const fieldSubscription = this.publicationEditionForm.controls[fieldName].valueChanges + .pipe( + map(value => value?.length ? value as string : '') + ) + .subscribe(fieldValue => { + switch (fieldName) { + case 'title': + this.publicationEditionService.editTitle(fieldValue); + break; + case 'description': + this.publicationEditionService.editDescription(fieldValue); + break; + case 'text': + this.publicationEditionService.editText(fieldValue); + break; + default: + break; + } + }); + this.subscriptions.push(fieldSubscription); + }); + + const publicationSubscription = this.publicationEditionService.state$.subscribe(state => { + this.publicationInEdition = state.publication; + this.publicationEditionForm.controls['title'].setValue(this.publication.title, { emitEvent: false }); + this.publicationEditionForm.controls['description'].setValue(this.publication.description, { emitEvent: false }); + this.publicationEditionForm.controls['text'].setValue(this.publication.text, { emitEvent: false }); + this.publicationEditionForm.controls['illustrationId'].setValue(this.publication.illustrationId, { emitEvent: false }); + this.publicationEditionForm.controls['categoryId'].setValue(this.publication.categoryId, { emitEvent: false }); + }); + this.subscriptions.push(publicationSubscription); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(subscription => subscription?.unsubscribe()); + } + + goPreviousLocation(): void { + this.location.back(); + } + + insertTitle(titleNumber: number): void { + this.publicationEditionService.insertTitle(titleNumber); + } + + selectAPicture(): void { + this.publicationEditionService.selectAPicture(); + } + + insertLink(): void { + this.publicationEditionService.insertLink(); + } + + displayCodeBlockDialog(): void { + this.publicationEditionService.displayCodeBlockDialog(); + } + + save(): void { + this.publicationSave.emit(this.publicationInEdition); + } + + displayPictureSectionDialog(): void { + this.publicationEditionService.displayPictureSectionDialog(); + } + + updateCursorPosition(event: KeyboardEvent | MouseEvent): void { + if (event.target) { + const textarea = event.target as HTMLTextAreaElement; + + const positionStart = textarea.selectionStart; + const positionEnd = textarea.selectionEnd; + + const selectedCharacterCount = positionEnd - positionStart; + console.log(`cursor position updated: [${positionStart}, ${positionEnd}] (${selectedCharacterCount})`); + this.publicationEditionService.editCursorPosition(positionStart, positionEnd); + } + } + + onTabChange(tabSelectedIndex: number): void { + if (tabSelectedIndex === 1) { + this.publicationEditionService.loadPreview(); + } + } +} \ No newline at end of file diff --git a/frontend/src/app/pages/publication-edition/publication-edition.service.ts b/frontend/src/app/components/publication-edition/publication-edition.service.ts similarity index 93% rename from frontend/src/app/pages/publication-edition/publication-edition.service.ts rename to frontend/src/app/components/publication-edition/publication-edition.service.ts index 4237efb..6e1a5a3 100644 --- a/frontend/src/app/pages/publication-edition/publication-edition.service.ts +++ b/frontend/src/app/components/publication-edition/publication-edition.service.ts @@ -1,14 +1,16 @@ -import { Location } from "@angular/common"; import { inject, Injectable, OnDestroy } from "@angular/core"; -import { MatSnackBar } from "@angular/material/snack-bar"; import { ActivatedRoute, Router } from "@angular/router"; -import { BehaviorSubject, Observable, Subscription, timeout } from "rxjs"; -import { Publication } from "../../core/rest-services/publications/model/publication"; import { PublicationRestService } from "../../core/rest-services/publications/publication.rest-service"; -import { copy } from '../../core/utils/ObjectUtils'; +import { MatSnackBar } from "@angular/material/snack-bar"; import { MatDialog } from "@angular/material/dialog"; -import { PictureSelectionDialog } from "./picture-selection-dialog/picture-selection-dialog.component"; -import { CodeBlockDialog } from "./code-block-dialog/code-block-dialog.component"; +import { BehaviorSubject, Observable, Subscription } from "rxjs"; +import { copy } from "../../core/utils/ObjectUtils"; +import { Publication } from "../../core/rest-services/publications/model/publication"; +import { Location } from "@angular/common"; +import { PictureSelectionDialog } from "../../pages/publication-edition/picture-selection-dialog/picture-selection-dialog.component"; +import { CodeBlockDialog } from "../../pages/publication-edition/code-block-dialog/code-block-dialog.component"; + +declare let Prism: any; export class CursorPosition { start: number; @@ -119,6 +121,12 @@ export class PublicationEditionService implements OnDestroy { }); } + init(publication: Publication): void { + const state = this._state; + state.publication = publication; + this.stateSubject.next(state); + } + editTitle(title: string): void { const state = this._state; state.publication.title = title; @@ -276,10 +284,13 @@ export class PublicationEditionService implements OnDestroy { .then(parsedText => { state.publication.parsedText = parsedText; this._save(state); + setTimeout(() => Prism.highlightAll(), 1000); }) .catch(error => { console.error(error); }) - .finally(() => this.isPreviewingSubject.next(false)); + .finally(() => { + this.isPreviewingSubject.next(false); + }); } } \ No newline at end of file diff --git a/frontend/src/app/pages/publication-edition/publication-edition.component.html b/frontend/src/app/pages/publication-edition/publication-edition.component.html deleted file mode 100644 index 242b1f8..0000000 --- a/frontend/src/app/pages/publication-edition/publication-edition.component.html +++ /dev/null @@ -1,97 +0,0 @@ -@if ((isLoading$ | async) == true) { - -} -@else { - @if (publication) { -
-
-

Modification de l'article {{ publication.title }}

-
- - - -
-
-
- - Title - - - - Description - - -
- -
- -
-
- -
- - - - - - - -
- - Content - - -
-
- - -
- @if ((isPreviewing$ | async) === true) { -

Preview is loading...

- - } @else { - -
-

{{ publication.title }}

-

{{ publication.description }}

-
-
- } -
-
-
-
- - -
-
- } - @else { -
-

Publication failed to load...

-
- } -} \ No newline at end of file diff --git a/frontend/src/app/pages/publication-edition/publication-edition.component.ts b/frontend/src/app/pages/publication-edition/publication-edition.component.ts deleted file mode 100644 index 2300241..0000000 --- a/frontend/src/app/pages/publication-edition/publication-edition.component.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { CommonModule, Location } from '@angular/common'; -import { Component, inject, OnDestroy, OnInit } from '@angular/core'; -import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { MatTabsModule } from '@angular/material/tabs'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { map, Observable, Subscription } from 'rxjs'; -import { SubmitButtonComponent } from '../../components/submit-button/submit-button.component'; -import { Publication } from '../../core/rest-services/publications/model/publication'; -import { PictureSelectionDialog } from './picture-selection-dialog/picture-selection-dialog.component'; -import { PublicationEditionService } from './publication-edition.service'; - -@Component({ - selector: 'app-publication-edition', - standalone: true, - imports: [ - CommonModule, - MatDialogModule, - MatIconModule, - MatInputModule, - MatProgressSpinnerModule, - MatTabsModule, - MatTooltipModule, - PictureSelectionDialog, - ReactiveFormsModule, - SubmitButtonComponent - ], - templateUrl: './publication-edition.component.html', - styleUrl: './publication-edition.component.scss', - providers: [PublicationEditionService] -}) -export class PublicationEditionComponent implements OnInit, OnDestroy { - private formBuilder = inject(FormBuilder); - private location = inject(Location); - private publicationEditionService = inject(PublicationEditionService); - - private subscriptions: Subscription[] = []; - publication!: Publication; - publicationEditionForm: FormGroup = this.formBuilder.group({ - title: new FormControl('', [Validators.required]), - description: new FormControl('', [Validators.required]), - text: new FormControl('', [Validators.required]), - illustrationId: new FormControl('', [Validators.required]), - categoryId: new FormControl('', [Validators.required]) - }); - - get isLoading$(): Observable { - return this.publicationEditionService.isLoading$; - } - - get isSaving$(): Observable { - return this.publicationEditionService.isSaving$; - } - - get isPreviewing$(): Observable { - return this.publicationEditionService.isPreviewing$; - } - - ngOnInit(): void { - ['title', 'description', 'text'].forEach(fieldName => { - const fieldSubscription = this.publicationEditionForm.controls[fieldName].valueChanges - .pipe( - map(value => value?.length ? value as string : '') - ) - .subscribe(fieldValue => { - switch (fieldName) { - case 'title': - this.publicationEditionService.editTitle(fieldValue); - break; - case 'description': - this.publicationEditionService.editDescription(fieldValue); - break; - case 'text': - this.publicationEditionService.editText(fieldValue); - break; - default: - break; - } - }); - this.subscriptions.push(fieldSubscription); - }); - - const publicationSubscription = this.publicationEditionService.state$.subscribe(state => { - this.publication = state.publication; - this.publicationEditionForm.controls['title'].setValue(this.publication.title, { emitEvent: false }); - this.publicationEditionForm.controls['description'].setValue(this.publication.description, { emitEvent: false }); - this.publicationEditionForm.controls['text'].setValue(this.publication.text, { emitEvent: false }); - this.publicationEditionForm.controls['illustrationId'].setValue(this.publication.illustrationId, { emitEvent: false }); - this.publicationEditionForm.controls['categoryId'].setValue(this.publication.categoryId, { emitEvent: false }); - }); - this.subscriptions.push(publicationSubscription); - - this.publicationEditionService.loadPublication(); - } - - ngOnDestroy(): void { - this.subscriptions.forEach(subscription => subscription?.unsubscribe()); - } - - goPreviousLocation(): void { - this.location.back(); - } - - insertTitle(titleNumber: number): void { - this.publicationEditionService.insertTitle(titleNumber); - } - - selectAPicture(): void { - this.publicationEditionService.selectAPicture(); - } - - insertLink(): void { - this.publicationEditionService.insertLink(); - } - - displayCodeBlockDialog(): void { - this.publicationEditionService.displayCodeBlockDialog(); - } - - save(): void { - this.publicationEditionService.save(); - } - - displayPictureSectionDialog(): void { - this.publicationEditionService.displayPictureSectionDialog(); - } - - updateCursorPosition(event: KeyboardEvent | MouseEvent): void { - if (event.target) { - const textarea = event.target as HTMLTextAreaElement; - - const positionStart = textarea.selectionStart; - const positionEnd = textarea.selectionEnd; - - const selectedCharacterCount = positionEnd - positionStart; - console.log(`cursor position updated: [${positionStart}, ${positionEnd}] (${selectedCharacterCount})`); - this.publicationEditionService.editCursorPosition(positionStart, positionEnd); - } - } - - onTabChange(tabSelectedIndex: number): void { - if (tabSelectedIndex === 1) { - this.publicationEditionService.loadPreview(); - } - } -} \ No newline at end of file diff --git a/frontend/src/app/pages/publication-edition/publication-edition.routes.ts b/frontend/src/app/pages/publication-edition/publication-edition.routes.ts deleted file mode 100644 index 8a1839a..0000000 --- a/frontend/src/app/pages/publication-edition/publication-edition.routes.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Route } from "@angular/router"; -import { PublicationEditionComponent } from "./publication-edition.component"; -import { authenticationGuard } from "../../core/guard/authentication.guard"; - -export const ROUTES: Route[] = [ - { path: '', component: PublicationEditionComponent, canActivate: [authenticationGuard] } -] \ No newline at end of file diff --git a/frontend/src/app/pages/publication-edition/publication-update.component.html b/frontend/src/app/pages/publication-edition/publication-update.component.html new file mode 100644 index 0000000..b690b72 --- /dev/null +++ b/frontend/src/app/pages/publication-edition/publication-update.component.html @@ -0,0 +1,13 @@ +@if ((isLoading$ | async) == true) { + +} +@else { + @if (publication) { + + } + @else { +
+

Publication failed to load...

+
+ } +} \ No newline at end of file diff --git a/frontend/src/app/pages/publication-edition/publication-update.component.scss b/frontend/src/app/pages/publication-edition/publication-update.component.scss new file mode 100644 index 0000000..71c44de --- /dev/null +++ b/frontend/src/app/pages/publication-edition/publication-update.component.scss @@ -0,0 +1,7 @@ +:host { + display: flex; + + app-publication-edition { + flex: 1; + } +} \ No newline at end of file diff --git a/frontend/src/app/pages/publication-edition/publication-update.component.ts b/frontend/src/app/pages/publication-edition/publication-update.component.ts new file mode 100644 index 0000000..7df69d4 --- /dev/null +++ b/frontend/src/app/pages/publication-edition/publication-update.component.ts @@ -0,0 +1,97 @@ +import { CommonModule, Location } from '@angular/common'; +import { Component, inject, OnDestroy, OnInit } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { PublicationEditionComponent } from '../../components/publication-edition/publication-edition.component'; +import { SubmitButtonComponent } from '../../components/submit-button/submit-button.component'; +import { Publication } from '../../core/rest-services/publications/model/publication'; +import { PublicationRestService } from '../../core/rest-services/publications/publication.rest-service'; +import { PictureSelectionDialog } from './picture-selection-dialog/picture-selection-dialog.component'; + +@Component({ + selector: 'app-publication-update', + standalone: true, + imports: [ + CommonModule, + MatDialogModule, + MatIconModule, + MatInputModule, + MatProgressSpinnerModule, + MatTabsModule, + MatTooltipModule, + PictureSelectionDialog, + ReactiveFormsModule, + SubmitButtonComponent, + PublicationEditionComponent + ], + templateUrl: './publication-update.component.html', + styleUrl: './publication-update.component.scss', +}) +export class PublicationUpdateComponent implements OnInit, OnDestroy { + private readonly publicationRestService = inject(PublicationRestService); + private readonly activatedRoute = inject(ActivatedRoute); + private readonly location = inject(Location); + private readonly router = inject(Router); + private readonly snackBar = inject(MatSnackBar); + private isLoadingSubject = new BehaviorSubject(false); + private isSavingSubject = new BehaviorSubject(false); + private subscriptions: Subscription[] = []; + publication: Publication | undefined; + + get isLoading$(): Observable { + return this.isLoadingSubject.asObservable(); + } + + get isSaving$(): Observable { + return this.isSavingSubject.asObservable(); + } + + ngOnInit(): void { + this.isLoadingSubject.next(true); + this.activatedRoute.paramMap.subscribe(params => { + const publicationId = params.get('publicationId'); + if (publicationId == undefined) { + this.snackBar.open('A technical error occurred while loading publication data.', 'Close', { duration: 5000 }); + this.location.back(); + } else { + this.publicationRestService.getById(publicationId) + .then(publication => { + this.publication = publication; + }) + .catch(error => { + const errorMessage = 'A technical error occurred while loading publication data.'; + this.snackBar.open(errorMessage, 'Close', { duration: 5000 }); + console.error(errorMessage, error) + }) + .finally(() => this.isLoadingSubject.next(false)); + } + }); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(subscription => subscription?.unsubscribe()); + } + + onPublicationSave(publication: Publication): void { + this.isSavingSubject.next(true); + this.publicationRestService.update(publication) + .then(() => { + this.snackBar.open('Publication updated succesfully!', 'Close', { duration: 5000 }); + this.router.navigate(['/home']); + }) + .catch(error => { + const errorMessage = 'An error occured while saving publication modifications.'; + console.error(errorMessage, error); + this.snackBar.open(errorMessage, 'Close', { duration: 5000 }); + }) + .finally(() => this.isSavingSubject.next(false)); + } +} \ No newline at end of file diff --git a/frontend/src/app/pages/publication-edition/publication-update.routes.ts b/frontend/src/app/pages/publication-edition/publication-update.routes.ts new file mode 100644 index 0000000..90e519c --- /dev/null +++ b/frontend/src/app/pages/publication-edition/publication-update.routes.ts @@ -0,0 +1,7 @@ +import { Route } from "@angular/router"; +import { PublicationUpdateComponent } from "./publication-update.component"; +import { authenticationGuard } from "../../core/guard/authentication.guard"; + +export const ROUTES: Route[] = [ + { path: '', component: PublicationUpdateComponent, canActivate: [authenticationGuard] } +] \ No newline at end of file