1- import { Component , OnInit , ViewEncapsulation , OnDestroy , HostListener } from '@angular/core' ;
1+ import { Component , OnInit , ViewEncapsulation , OnDestroy , HostListener , ViewChild } from '@angular/core' ;
22import { FormArray , FormControl , FormGroup , NonNullableFormBuilder , Validators , FormsModule , ReactiveFormsModule } from '@angular/forms' ;
33import { ActivatedRoute , Router } from '@angular/router' ;
44import { combineLatest , forkJoin , Subject , interval , of , race } from 'rxjs' ;
5- import { catchError , takeUntil , debounce , filter , startWith , take } from 'rxjs/operators' ;
5+ import { catchError , takeUntil , debounce , filter , startWith , take , switchMap } from 'rxjs/operators' ;
66import { CouchService } from '../../shared/couchdb.service' ;
77import { UserService } from '../../shared/user.service' ;
88import { PlanetMessageService } from '../../shared/planet-message.service' ;
@@ -15,6 +15,7 @@ import { PlanetStepListService, PlanetStepListComponent, PlanetStepListItemCompo
1515import { showFormErrors } from '../../shared/table-helpers' ;
1616import { CanComponentDeactivate } from '../../shared/unsaved-changes.guard' ;
1717import { warningMsg } from '../../shared/unsaved-changes.component' ;
18+ import { FileInputComponent } from '../../shared/forms/file-input.component' ;
1819import { MatToolbar } from '@angular/material/toolbar' ;
1920import { MatIconButton , MatAnchor , MatButton } from '@angular/material/button' ;
2021import { MatIcon } from '@angular/material/icon' ;
@@ -82,22 +83,30 @@ type LinkFormGroup = FormGroup<LinkFormControls>;
8283 MatToolbar , MatIconButton , MatIcon , NgIf , FormsModule , ReactiveFormsModule , MatFormField , MatLabel ,
8384 MatInput , MatError , FormErrorMessagesComponent , MatDatepickerInput , MatDatepickerToggle , MatSuffix , MatDatepicker ,
8485 PlanetMarkdownTextboxComponent , MatAnchor , NgSwitch , NgSwitchCase , PlanetStepListComponent , NgFor ,
85- PlanetStepListItemComponent , MatListItemTitle , MatListItemMeta , MatButton , MatCheckbox , SubmitDirective
86+ PlanetStepListItemComponent , MatListItemTitle , MatListItemMeta , MatButton , MatCheckbox , SubmitDirective , FileInputComponent
8687 ]
8788} )
8889export class UsersAchievementsUpdateComponent implements OnInit , OnDestroy , CanComponentDeactivate {
8990 user = this . userService . get ( ) ;
9091 configuration = this . stateService . configuration ;
9192 docInfo = { '_id' : this . user . _id + '@' + this . configuration . code , '_rev' : undefined } ;
9293 readonly dbName = 'achievements' ;
94+ readonly resumeAttachmentKey = 'resume.pdf' ;
95+ readonly maxResumeSizeMb = 512 ;
9396 achievementNotFound = false ;
9497 editForm ! : FormGroup < EditFormControls > ;
9598 profileForm ! : FormGroup < ProfileFormControls > ;
9699 private onDestroy$ = new Subject < void > ( ) ;
97100 initialFormValues : any ;
98101 hasUnsavedChanges = false ;
99102 submitAttempted = false ;
103+ currentResumeFileName = '' ;
104+ resumeFile : File | null = null ;
105+ resumeUploadError = '' ;
106+ resumeMarkedForDeletion = false ;
107+ existingResumeAttachment : any = null ;
100108 private submitAfterPending = false ;
109+ @ViewChild ( 'resumeInput' ) resumeInput ?: FileInputComponent ;
101110 get achievements ( ) : FormArray < AchievementFormGroup > {
102111 return this . editForm . controls . achievements ;
103112 }
@@ -145,6 +154,9 @@ export class UsersAchievementsUpdateComponent implements OnInit, OnDestroy, CanC
145154 this . editForm . setControl ( 'links' , this . buildLinksFormArray ( achievements . links ) ) ;
146155 // Keeping older otherInfo property so we don't lose this info on database
147156 this . editForm . setControl ( 'otherInfo' , this . buildOtherInfoFormArray ( achievements . otherInfo ) ) ;
157+ this . currentResumeFileName = achievements . resumeFileName || ( achievements . _attachments ?. [ this . resumeAttachmentKey ] ?
158+ this . resumeAttachmentKey : '' ) ;
159+ this . existingResumeAttachment = achievements . _attachments ?. [ this . resumeAttachmentKey ] || null ;
148160
149161 if ( this . docInfo . _id === achievements . _id ) {
150162 this . docInfo . _rev = achievements . _rev ;
@@ -164,11 +176,7 @@ export class UsersAchievementsUpdateComponent implements OnInit, OnDestroy, CanC
164176 }
165177
166178 private captureInitialState ( ) {
167- const editFormState = this . editForm . getRawValue ( ) ;
168- this . initialFormValues = JSON . stringify ( {
169- editForm : editFormState ,
170- profileForm : this . profileForm . getRawValue ( )
171- } ) ;
179+ this . initialFormValues = this . getCurrentState ( ) ;
172180 }
173181
174182 onFormChanges ( ) {
@@ -269,12 +277,19 @@ export class UsersAchievementsUpdateComponent implements OnInit, OnDestroy, CanC
269277 }
270278
271279 private updateUnsavedChangesFlag ( ) {
272- const editFormState = this . editForm . getRawValue ( ) ;
273- const currentState = JSON . stringify ( {
274- editForm : editFormState ,
275- profileForm : this . profileForm . getRawValue ( )
280+ this . hasUnsavedChanges = this . getCurrentState ( ) !== this . initialFormValues ;
281+ }
282+
283+ private getCurrentState ( ) {
284+ return JSON . stringify ( {
285+ editForm : this . editForm . getRawValue ( ) ,
286+ profileForm : this . profileForm . getRawValue ( ) ,
287+ resume : {
288+ fileName : this . currentResumeFileName ,
289+ hasPendingUpload : ! ! this . resumeFile ,
290+ markedForDeletion : this . resumeMarkedForDeletion
291+ }
276292 } ) ;
277- this . hasUnsavedChanges = currentState !== this . initialFormValues ;
278293 }
279294
280295 addAchievement ( index = - 1 , achievement = { title : '' , description : '' , link : '' , date : '' } ) {
@@ -364,8 +379,58 @@ export class UsersAchievementsUpdateComponent implements OnInit, OnDestroy, CanC
364379 } ) ;
365380 }
366381
382+ onResumeSelected ( event : Event ) {
383+ const input = event . target as HTMLInputElement ;
384+ const file = input . files ?. [ 0 ] ?? null ;
385+ if ( ! file ) {
386+ this . resumeFile = null ;
387+ this . resumeUploadError = '' ;
388+ this . updateUnsavedChangesFlag ( ) ;
389+ return ;
390+ }
391+
392+ const isPdf = file . type === 'application/pdf' || file . name . toLowerCase ( ) . endsWith ( '.pdf' ) ;
393+ if ( ! isPdf ) {
394+ this . resumeFile = null ;
395+ this . resumeUploadError = $localize `Please select a PDF file` ;
396+ this . resumeInput ?. clearFile ( ) ;
397+ this . updateUnsavedChangesFlag ( ) ;
398+ return ;
399+ }
400+
401+ if ( file . size / 1024 / 1024 > this . maxResumeSizeMb ) {
402+ this . resumeFile = null ;
403+ this . resumeUploadError = $localize `Please select a PDF file smaller than ${ this . maxResumeSizeMb } MB` ;
404+ this . resumeInput ?. clearFile ( ) ;
405+ this . updateUnsavedChangesFlag ( ) ;
406+ return ;
407+ }
408+
409+ this . resumeFile = file ;
410+ this . resumeUploadError = '' ;
411+ this . resumeMarkedForDeletion = false ;
412+ this . updateUnsavedChangesFlag ( ) ;
413+ }
414+
415+ clearResumeSelection ( ) {
416+ this . resumeFile = null ;
417+ this . resumeUploadError = '' ;
418+ this . resumeInput ?. clearFile ( ) ;
419+ this . updateUnsavedChangesFlag ( ) ;
420+ }
421+
422+ removeExistingResume ( ) {
423+ this . resumeMarkedForDeletion = true ;
424+ this . currentResumeFileName = '' ;
425+ this . clearResumeSelection ( ) ;
426+ }
427+
367428 onSubmit ( ) {
368429 this . submitAttempted = true ;
430+ if ( this . resumeUploadError ) {
431+ this . planetMessageService . showAlert ( $localize `Please upload your CV/Resume as a PDF file` ) ;
432+ return ;
433+ }
369434 if ( this . editForm . pending || this . profileForm . pending ) {
370435 if ( this . submitAfterPending ) {
371436 return ;
@@ -428,12 +493,43 @@ export class UsersAchievementsUpdateComponent implements OnInit, OnDestroy, CanC
428493 }
429494
430495 updateAchievements ( docInfo , achievements , userInfo ) {
431- // ...is the rest syntax for object destructuring
432- forkJoin ( [
433- this . couchService . post ( this . dbName , { ...docInfo , ...achievements ,
434- 'createdOn' : this . configuration . code , 'username' : this . user . name , 'parentCode' : this . configuration . parentCode } ) ,
435- this . userService . updateUser ( userInfo )
436- ] ) . subscribe ( ( ) => {
496+ const achievementsDoc : any = {
497+ ...docInfo ,
498+ ...achievements ,
499+ 'createdOn' : this . configuration . code ,
500+ 'username' : this . user . name ,
501+ 'parentCode' : this . configuration . parentCode
502+ } ;
503+
504+ if ( this . resumeFile ) {
505+ achievementsDoc . resumeFileName = this . resumeFile . name ;
506+ if ( this . existingResumeAttachment ) {
507+ achievementsDoc . _attachments = {
508+ [ this . resumeAttachmentKey ] : this . existingResumeAttachment
509+ } ;
510+ }
511+ } else if ( ! this . resumeMarkedForDeletion && this . currentResumeFileName ) {
512+ achievementsDoc . resumeFileName = this . currentResumeFileName ;
513+ if ( this . existingResumeAttachment ) {
514+ achievementsDoc . _attachments = {
515+ [ this . resumeAttachmentKey ] : this . existingResumeAttachment
516+ } ;
517+ }
518+ }
519+
520+ this . couchService . post ( this . dbName , achievementsDoc ) . pipe (
521+ switchMap ( ( achievementsRes ) => forkJoin ( [
522+ this . resumeFile ?
523+ this . couchService . putAttachment (
524+ this . dbName + '/' + achievementsRes . id + '/' + this . resumeAttachmentKey + '?rev=' + achievementsRes . rev ,
525+ this . resumeFile , { headers : { 'Content-Type' : this . resumeFile . type } }
526+ ) :
527+ of ( { } ) ,
528+ this . userService . updateUser ( userInfo )
529+ ] ) )
530+ ) . subscribe ( ( ) => {
531+ this . resumeFile = null ;
532+ this . resumeMarkedForDeletion = false ;
437533 this . planetMessageService . showMessage ( $localize `Achievements successfully updated` ) ;
438534 this . goBack ( ) ;
439535 } , ( err ) => {
0 commit comments