Extract publication edition into a separated component.
This commit is contained in:
@@ -19,7 +19,7 @@ export const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'publications/:publicationId/edit',
|
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',
|
path: 'publications',
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<form [formGroup]="publicationEditionForm" (submit)="save()" ngNativeValidate>
|
||||||
|
<header>
|
||||||
|
<h1>Modification de l'article {{ publication.title }}</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<mat-tab-group dynamicHeight (selectedIndexChange)="onTabChange($event)">
|
||||||
|
<mat-tab label="Edition">
|
||||||
|
<div class="form-content">
|
||||||
|
<div class="first-part">
|
||||||
|
<div>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Title</mat-label>
|
||||||
|
<input matInput type="text" formControlName="title" />
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Description</mat-label>
|
||||||
|
<input matInput type="text" formControlName="description" />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="picture-container">
|
||||||
|
<img src="/api/pictures/{{publication.illustrationId}}" (click)="displayPictureSectionDialog()" matTooltip="Click to change illustration"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" matTooltip="Click to insert a title 1 section" (click)="insertTitle(1)">
|
||||||
|
H1
|
||||||
|
</button>
|
||||||
|
<button type="button" matTooltip="Click to insert a title 2 section" (click)="insertTitle(2)">
|
||||||
|
H2
|
||||||
|
</button>
|
||||||
|
<button type="button" matTooltip="Click to insert a title 1 section" (click)="insertTitle(3)">
|
||||||
|
H3
|
||||||
|
</button>
|
||||||
|
<button type="button" matTooltip="Click to insert a picture" (click)="selectAPicture()">
|
||||||
|
<mat-icon>image</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button type="button" matTooltip="Click to insert a link" (click)="insertLink()">
|
||||||
|
<mat-icon>link</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button type="button" matTooltip="Click to insert a code block" (click)="displayCodeBlockDialog()">
|
||||||
|
<mat-icon>code</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button type="button" disabled matTooltip="Click to display editor help">
|
||||||
|
<mat-icon>help</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<mat-form-field class="example-form-field">
|
||||||
|
<mat-label>Content</mat-label>
|
||||||
|
<textarea
|
||||||
|
#textArea
|
||||||
|
matInput
|
||||||
|
formControlName="text"
|
||||||
|
class="text-input"
|
||||||
|
(keyup)="updateCursorPosition($event)"
|
||||||
|
(click)="updateCursorPosition($event)">
|
||||||
|
</textarea>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</mat-tab>
|
||||||
|
|
||||||
|
<mat-tab label="Previewing">
|
||||||
|
<div class="preview">
|
||||||
|
@if ((isPreviewing$ | async) === true) {
|
||||||
|
<div class="preview-loading">
|
||||||
|
<h2>Preview is loading...</h2>
|
||||||
|
<mat-spinner></mat-spinner>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<img class="illustration" src="/pictures/{{ publication.illustrationId }}" />
|
||||||
|
<header>
|
||||||
|
<h1>{{ publication.title }}</h1>
|
||||||
|
<h2>{{ publication.description }}</h2>
|
||||||
|
</header>
|
||||||
|
<main [innerHTML]="publication.parsedText"></main>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</mat-tab>
|
||||||
|
</mat-tab-group>
|
||||||
|
<footer>
|
||||||
|
<app-submit-button label="Save" [requestPending]="!!(isSaving$ | async)"></app-submit-button>
|
||||||
|
<button type="button" class="secondary" (click)="goPreviousLocation()">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
@@ -113,10 +113,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::ng-deep .test circle {
|
|
||||||
stroke: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
button, a.button {
|
button, a.button {
|
||||||
padding: .8em 1.2em;
|
padding: .8em 1.2em;
|
||||||
border-radius: 10em;
|
border-radius: 10em;
|
||||||
@@ -151,9 +147,15 @@ button, a.button {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
align-items: center;
|
|
||||||
|
.preview-loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.illustration {
|
.illustration {
|
||||||
|
flex: 1;
|
||||||
height: 12em;
|
height: 12em;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
transition: height .2s ease-in-out;
|
transition: height .2s ease-in-out;
|
||||||
@@ -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<boolean> = of(false);
|
||||||
|
@Output()
|
||||||
|
publicationSave = new EventEmitter<Publication>();
|
||||||
|
|
||||||
|
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<string | undefined>('', [Validators.required]),
|
||||||
|
description: new FormControl<string | undefined>('', [Validators.required]),
|
||||||
|
text: new FormControl<string | undefined>('', [Validators.required]),
|
||||||
|
illustrationId: new FormControl<string | undefined>('', [Validators.required]),
|
||||||
|
categoryId: new FormControl<string | undefined>('', [Validators.required])
|
||||||
|
});
|
||||||
|
|
||||||
|
get isLoading$(): Observable<boolean> {
|
||||||
|
return this.publicationEditionService.isLoading$;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isPreviewing$(): Observable<boolean> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import { Location } from "@angular/common";
|
|
||||||
import { inject, Injectable, OnDestroy } from "@angular/core";
|
import { inject, Injectable, OnDestroy } from "@angular/core";
|
||||||
import { MatSnackBar } from "@angular/material/snack-bar";
|
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
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 { 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 { MatDialog } from "@angular/material/dialog";
|
||||||
import { PictureSelectionDialog } from "./picture-selection-dialog/picture-selection-dialog.component";
|
import { BehaviorSubject, Observable, Subscription } from "rxjs";
|
||||||
import { CodeBlockDialog } from "./code-block-dialog/code-block-dialog.component";
|
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 {
|
export class CursorPosition {
|
||||||
start: number;
|
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 {
|
editTitle(title: string): void {
|
||||||
const state = this._state;
|
const state = this._state;
|
||||||
state.publication.title = title;
|
state.publication.title = title;
|
||||||
@@ -276,10 +284,13 @@ export class PublicationEditionService implements OnDestroy {
|
|||||||
.then(parsedText => {
|
.then(parsedText => {
|
||||||
state.publication.parsedText = parsedText;
|
state.publication.parsedText = parsedText;
|
||||||
this._save(state);
|
this._save(state);
|
||||||
|
setTimeout(() => Prism.highlightAll(), 1000);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
})
|
})
|
||||||
.finally(() => this.isPreviewingSubject.next(false));
|
.finally(() => {
|
||||||
|
this.isPreviewingSubject.next(false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
@if ((isLoading$ | async) == true) {
|
|
||||||
<mat-spinner></mat-spinner>
|
|
||||||
}
|
|
||||||
@else {
|
|
||||||
@if (publication) {
|
|
||||||
<form [formGroup]="publicationEditionForm" (submit)="save()" ngNativeValidate>
|
|
||||||
<header>
|
|
||||||
<h1>Modification de l'article {{ publication.title }}</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<mat-tab-group dynamicHeight (selectedIndexChange)="onTabChange($event)">
|
|
||||||
<mat-tab label="Edition">
|
|
||||||
<div class="form-content">
|
|
||||||
<div class="first-part">
|
|
||||||
<div>
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>Title</mat-label>
|
|
||||||
<input matInput type="text" formControlName="title" />
|
|
||||||
</mat-form-field>
|
|
||||||
<mat-form-field>
|
|
||||||
<mat-label>Description</mat-label>
|
|
||||||
<input matInput type="text" formControlName="description" />
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="picture-container">
|
|
||||||
<img src="/api/pictures/{{publication.illustrationId}}" (click)="displayPictureSectionDialog()" matTooltip="Click to change illustration"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<button type="button" matTooltip="Click to insert a title 1 section" (click)="insertTitle(1)">
|
|
||||||
H1
|
|
||||||
</button>
|
|
||||||
<button type="button" matTooltip="Click to insert a title 2 section" (click)="insertTitle(2)">
|
|
||||||
H2
|
|
||||||
</button>
|
|
||||||
<button type="button" matTooltip="Click to insert a title 1 section" (click)="insertTitle(3)">
|
|
||||||
H3
|
|
||||||
</button>
|
|
||||||
<button type="button" matTooltip="Click to insert a picture" (click)="selectAPicture()">
|
|
||||||
<mat-icon>image</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button type="button" matTooltip="Click to insert a link" (click)="insertLink()">
|
|
||||||
<mat-icon>link</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button type="button" matTooltip="Click to insert a code block" (click)="displayCodeBlockDialog()">
|
|
||||||
<mat-icon>code</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button type="button" disabled matTooltip="Click to display editor help">
|
|
||||||
<mat-icon>help</mat-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<mat-form-field class="example-form-field">
|
|
||||||
<mat-label>Content</mat-label>
|
|
||||||
<textarea
|
|
||||||
#textArea
|
|
||||||
matInput
|
|
||||||
formControlName="text"
|
|
||||||
class="text-input"
|
|
||||||
(keyup)="updateCursorPosition($event)"
|
|
||||||
(click)="updateCursorPosition($event)">
|
|
||||||
</textarea>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</mat-tab>
|
|
||||||
|
|
||||||
<mat-tab label="Previewing">
|
|
||||||
<div class="preview">
|
|
||||||
@if ((isPreviewing$ | async) === true) {
|
|
||||||
<h2>Preview is loading...</h2>
|
|
||||||
<mat-spinner></mat-spinner>
|
|
||||||
} @else {
|
|
||||||
<img class="illustration" src="/pictures/{{ publication.illustrationId }}" />
|
|
||||||
<header>
|
|
||||||
<h1>{{ publication.title }}</h1>
|
|
||||||
<h2>{{ publication.description }}</h2>
|
|
||||||
</header>
|
|
||||||
<main [innerHTML]="publication.parsedText"></main>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</mat-tab>
|
|
||||||
</mat-tab-group>
|
|
||||||
<footer>
|
|
||||||
<app-submit-button label="Save" [requestPending]="(isSaving$ | async) == true"></app-submit-button>
|
|
||||||
<button type="button" class="secondary" (click)="goPreviousLocation()">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
@else {
|
|
||||||
<div class="loading-failed">
|
|
||||||
<h1>Publication failed to load...</h1>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<string | undefined>('', [Validators.required]),
|
|
||||||
description: new FormControl<string | undefined>('', [Validators.required]),
|
|
||||||
text: new FormControl<string | undefined>('', [Validators.required]),
|
|
||||||
illustrationId: new FormControl<string | undefined>('', [Validators.required]),
|
|
||||||
categoryId: new FormControl<string | undefined>('', [Validators.required])
|
|
||||||
});
|
|
||||||
|
|
||||||
get isLoading$(): Observable<boolean> {
|
|
||||||
return this.publicationEditionService.isLoading$;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isSaving$(): Observable<boolean> {
|
|
||||||
return this.publicationEditionService.isSaving$;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isPreviewing$(): Observable<boolean> {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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] }
|
|
||||||
]
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
@if ((isLoading$ | async) == true) {
|
||||||
|
<mat-spinner></mat-spinner>
|
||||||
|
}
|
||||||
|
@else {
|
||||||
|
@if (publication) {
|
||||||
|
<app-publication-edition [publication]="publication" [isSaving$]="isSaving$" (publicationSave)="onPublicationSave($event)"></app-publication-edition>
|
||||||
|
}
|
||||||
|
@else {
|
||||||
|
<div class="loading-failed">
|
||||||
|
<h1>Publication failed to load...</h1>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
app-publication-edition {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<boolean>(false);
|
||||||
|
private isSavingSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
private subscriptions: Subscription[] = [];
|
||||||
|
publication: Publication | undefined;
|
||||||
|
|
||||||
|
get isLoading$(): Observable<boolean> {
|
||||||
|
return this.isLoadingSubject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isSaving$(): Observable<boolean> {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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] }
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user