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 @@
+
\ 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) {
-
- }
- @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