Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/shared/couchdb.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class CouchService {
return this.couchDBReq('delete', db, this.setOpts(opts));
}

putAttachment(db: string, file: FormData, opts?: any) {
putAttachment(db: string, file: File | FormData, opts?: any) {
return this.couchDBReq('put', db, this.setOpts(opts), file);
}

Expand Down
18 changes: 12 additions & 6 deletions src/app/shared/forms/file-input.component.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import { Component, Output, EventEmitter, ViewChild } from '@angular/core';
import { Component, Output, EventEmitter, ViewChild, Input, ElementRef } from '@angular/core';
import { truncateText } from '../../shared/utils';

@Component({
selector: 'planet-file-input',
template: `
<div class="inner-gaps by-column">
<div class="inner-gaps by-column file-input-container">
<button type="button" mat-raised-button (click)="fileInput.click()" color="primary" i18n>Choose File</button>
<input hidden (change)="onFileSelected($event)" #fileInput type="file">
<input hidden (change)="onFileSelected($event)" #fileInput type="file" [accept]="accept">
<span class="file-name" i18n>{{ getTruncatedFileName() }}</span>
</div>
`,
styles: [ `
.file-name {
display: inline-flex;
align-items: center;
}
` ],
standalone: false
})
export class FileInputComponent {

@Input() accept = '';
@Output() fileChange = new EventEmitter<any>();
@ViewChild('fileInput') fileInput!: HTMLInputElement;
@ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;

selectedFile: any = null;
onFileSelected(event: any): void {
Expand All @@ -33,9 +40,8 @@ export class FileInputComponent {
clearFile() {
this.selectedFile = null;
if (this.fileInput) {
this.fileInput.value = '';
this.fileInput.nativeElement.value = '';
}
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,24 @@
<p class="warn-text-color mat-caption" *ngIf="sectionError('links')">{{ sectionError('links') }}</p>
<button type="button" (click)="addLink()" mat-stroked-button color="primary" i18n>Enter a Link</button>
</div>
<div>
<p class="mat-hint mat-caption" i18n>Upload your CV/Resume below (PDF only)</p>
<div class="existing-file-container">
<planet-file-input #resumeInput accept=".pdf,application/pdf" (fileChange)="onResumeSelected($event)"></planet-file-input>
<button mat-icon-button type="button" *ngIf="resumeFile" (click)="clearResumeSelection()" i18n-matTooltip matTooltip="Remove new attachment">
<mat-icon color="warn">delete</mat-icon>
</button>
</div>
<div class="existing-file-container" *ngIf="currentResumeFileName">
<p class="mat-caption existing-resume">
<ng-container i18n>Current CV/Resume:</ng-container> {{ currentResumeFileName }}
</p>
<button mat-icon-button type="button" (click)="removeExistingResume()" i18n-matTooltip matTooltip="Remove existing attachment">
<mat-icon color="warn">delete</mat-icon>
</button>
</div>
<p class="warn-text-color mat-caption" *ngIf="resumeUploadError">{{ resumeUploadError }}</p>
</div>
<mat-checkbox formControlName="sendToNation" class="full-width achievements-checkbox" i18n>
Allow your achievements to be shared with the nation
</mat-checkbox>
Expand Down
132 changes: 114 additions & 18 deletions src/app/users/users-achievements/users-achievements-update.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Component, OnInit, ViewEncapsulation, OnDestroy, HostListener } from '@angular/core';
import { Component, OnInit, ViewEncapsulation, OnDestroy, HostListener, ViewChild } from '@angular/core';
import { FormArray, FormControl, FormGroup, NonNullableFormBuilder, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { combineLatest, forkJoin, Subject, interval, of, race } from 'rxjs';
import { catchError, takeUntil, debounce, filter, startWith, take } from 'rxjs/operators';
import { catchError, takeUntil, debounce, filter, startWith, take, switchMap } from 'rxjs/operators';
import { CouchService } from '../../shared/couchdb.service';
import { UserService } from '../../shared/user.service';
import { PlanetMessageService } from '../../shared/planet-message.service';
Expand All @@ -15,6 +15,7 @@ import { PlanetStepListService } from '../../shared/forms/planet-step-list.compo
import { showFormErrors } from '../../shared/table-helpers';
import { CanComponentDeactivate } from '../../shared/unsaved-changes.guard';
import { warningMsg } from '../../shared/unsaved-changes.component';
import { FileInputComponent } from '../../shared/forms/file-input.component';

type DateValue = string | Date;
type DateSortOrder = 'none' | 'asc' | 'desc';
Expand Down Expand Up @@ -73,14 +74,22 @@ export class UsersAchievementsUpdateComponent implements OnInit, OnDestroy, CanC
configuration = this.stateService.configuration;
docInfo = { '_id': this.user._id + '@' + this.configuration.code, '_rev': undefined };
readonly dbName = 'achievements';
readonly resumeAttachmentKey = 'resume.pdf';
readonly maxResumeSizeMb = 512;
achievementNotFound = false;
editForm!: FormGroup<EditFormControls>;
profileForm!: FormGroup<ProfileFormControls>;
private onDestroy$ = new Subject<void>();
initialFormValues: any;
hasUnsavedChanges = false;
submitAttempted = false;
currentResumeFileName = '';
resumeFile: File | null = null;
resumeUploadError = '';
resumeMarkedForDeletion = false;
existingResumeAttachment: any = null;
private submitAfterPending = false;
@ViewChild('resumeInput') resumeInput?: FileInputComponent;
get achievements(): FormArray<AchievementFormGroup> {
return this.editForm.controls.achievements;
}
Expand Down Expand Up @@ -128,6 +137,9 @@ export class UsersAchievementsUpdateComponent implements OnInit, OnDestroy, CanC
this.editForm.setControl('links', this.buildLinksFormArray(achievements.links));
// Keeping older otherInfo property so we don't lose this info on database
this.editForm.setControl('otherInfo', this.buildOtherInfoFormArray(achievements.otherInfo));
this.currentResumeFileName = achievements.resumeFileName || (achievements._attachments?.[this.resumeAttachmentKey] ?
this.resumeAttachmentKey : '');
this.existingResumeAttachment = achievements._attachments?.[this.resumeAttachmentKey] || null;

if (this.docInfo._id === achievements._id) {
this.docInfo._rev = achievements._rev;
Expand All @@ -147,11 +159,7 @@ export class UsersAchievementsUpdateComponent implements OnInit, OnDestroy, CanC
}

private captureInitialState() {
const editFormState = this.editForm.getRawValue();
this.initialFormValues = JSON.stringify({
editForm: editFormState,
profileForm: this.profileForm.getRawValue()
});
this.initialFormValues = this.getCurrentState();
}

onFormChanges() {
Expand Down Expand Up @@ -252,12 +260,19 @@ export class UsersAchievementsUpdateComponent implements OnInit, OnDestroy, CanC
}

private updateUnsavedChangesFlag() {
const editFormState = this.editForm.getRawValue();
const currentState = JSON.stringify({
editForm: editFormState,
profileForm: this.profileForm.getRawValue()
this.hasUnsavedChanges = this.getCurrentState() !== this.initialFormValues;
}

private getCurrentState() {
return JSON.stringify({
editForm: this.editForm.getRawValue(),
profileForm: this.profileForm.getRawValue(),
resume: {
fileName: this.currentResumeFileName,
hasPendingUpload: !!this.resumeFile,
markedForDeletion: this.resumeMarkedForDeletion
}
});
this.hasUnsavedChanges = currentState !== this.initialFormValues;
}

addAchievement(index = -1, achievement = { title: '', description: '', link: '', date: '' }) {
Expand Down Expand Up @@ -347,8 +362,58 @@ export class UsersAchievementsUpdateComponent implements OnInit, OnDestroy, CanC
});
}

onResumeSelected(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0] ?? null;
if (!file) {
this.resumeFile = null;
this.resumeUploadError = '';
this.updateUnsavedChangesFlag();
return;
}

const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf');
if (!isPdf) {
this.resumeFile = null;
this.resumeUploadError = $localize`Please select a PDF file`;
this.resumeInput?.clearFile();
this.updateUnsavedChangesFlag();
return;
}

if (file.size / 1024 / 1024 > this.maxResumeSizeMb) {
this.resumeFile = null;
this.resumeUploadError = $localize`Please select a PDF file smaller than ${this.maxResumeSizeMb} MB`;
this.resumeInput?.clearFile();
this.updateUnsavedChangesFlag();
return;
}

this.resumeFile = file;
this.resumeUploadError = '';
this.resumeMarkedForDeletion = false;
this.updateUnsavedChangesFlag();
}

clearResumeSelection() {
this.resumeFile = null;
this.resumeUploadError = '';
this.resumeInput?.clearFile();
this.updateUnsavedChangesFlag();
}

removeExistingResume() {
this.resumeMarkedForDeletion = true;
this.currentResumeFileName = '';
this.clearResumeSelection();
}

onSubmit() {
this.submitAttempted = true;
if (this.resumeUploadError) {
this.planetMessageService.showAlert($localize`Please upload your CV/Resume as a PDF file`);
return;
}
if (this.editForm.pending || this.profileForm.pending) {
if (this.submitAfterPending) {
return;
Expand Down Expand Up @@ -411,12 +476,43 @@ export class UsersAchievementsUpdateComponent implements OnInit, OnDestroy, CanC
}

updateAchievements(docInfo, achievements, userInfo) {
// ...is the rest syntax for object destructuring
forkJoin([
this.couchService.post(this.dbName, { ...docInfo, ...achievements,
'createdOn': this.configuration.code, 'username': this.user.name, 'parentCode': this.configuration.parentCode }),
this.userService.updateUser(userInfo)
]).subscribe(() => {
const achievementsDoc: any = {
...docInfo,
...achievements,
'createdOn': this.configuration.code,
'username': this.user.name,
'parentCode': this.configuration.parentCode
};

if (this.resumeFile) {
achievementsDoc.resumeFileName = this.resumeFile.name;
if (this.existingResumeAttachment) {
achievementsDoc._attachments = {
[this.resumeAttachmentKey]: this.existingResumeAttachment
};
}
} else if (!this.resumeMarkedForDeletion && this.currentResumeFileName) {
achievementsDoc.resumeFileName = this.currentResumeFileName;
if (this.existingResumeAttachment) {
achievementsDoc._attachments = {
[this.resumeAttachmentKey]: this.existingResumeAttachment
};
}
}

this.couchService.post(this.dbName, achievementsDoc).pipe(
switchMap((achievementsRes) => forkJoin([
this.resumeFile ?
this.couchService.putAttachment(
this.dbName + '/' + achievementsRes.id + '/' + this.resumeAttachmentKey + '?rev=' + achievementsRes.rev,
this.resumeFile, { headers: { 'Content-Type': this.resumeFile.type } }
) :
of({}),
this.userService.updateUser(userInfo)
]))
).subscribe(() => {
this.resumeFile = null;
this.resumeMarkedForDeletion = false;
this.planetMessageService.showMessage($localize`Achievements successfully updated`);
this.goBack();
}, (err) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
width: 100%;
}

.existing-file-container {
display: flex;
align-items: center;
}

.achievements-checkbox {
margin-top: 15px;
margin-bottom: 15px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
</mat-toolbar>

<div class="space-container">
<mat-toolbar class="primary-color font-size-1 responsive-toolbar center-text">
<mat-toolbar class="primary-color font-size-1 responsive-toolbar">
<div>
<span *ngIf="user?.firstName; else elseBlock" class="center-text">{{ (user.firstName + ' ' + user.middleName + ' ' + user.lastName) | truncateText:40 }}</span>
<span *ngIf="user?.firstName; else elseBlock">{{ (user.firstName + ' ' + user.middleName + ' ' + user.lastName) | truncateText:40 }}</span>
<ng-template #elseBlock>{{ user.name | truncateText:40 }}</ng-template>
</div>
<span class="toolbar-fill"></span>
<div class="auto-adjust-buttons">
<a mat-raised-button color="primary" class="margin-r-1" *ngIf="ownAchievements && !achievementNotFound" (click)="generatePDF()">
<a mat-stroked-button class="margin-r-1" *ngIf="resumeUrl" [href]="resumeUrl" target="_blank" rel="noopener noreferrer" i18n>
View CV/Resume
</a>
<a mat-stroked-button class="margin-r-1" *ngIf="ownAchievements && !achievementNotFound" (click)="generatePDF()">
<span i18n>Print Achievements</span>
</a>
<a mat-raised-button color="accent" routerLink="update" *ngIf="ownAchievements">
Expand Down
10 changes: 10 additions & 0 deletions src/app/users/users-achievements/users-achievements.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pdfMake.addVirtualFileSystem(pdfFonts);
standalone: false
})
export class UsersAchievementsComponent implements OnInit {
readonly dbName = 'achievements';
readonly resumeAttachmentKey = 'resume.pdf';
user: any = {};
achievements: any;
achievementNotFound = false;
Expand Down Expand Up @@ -118,6 +120,14 @@ export class UsersAchievementsComponent implements OnInit {
this.openAchievementIndex = this.openAchievementIndex === index ? -1 : index;
}


get resumeUrl() {
if (!this.achievements?._attachments?.[this.resumeAttachmentKey] || !this.achievements?._id) {
return '';
}
return `${environment.couchAddress}/${this.dbName}/${this.achievements._id}/${this.resumeAttachmentKey}`;
}

get profileImg() {
const attachments = this.userService.get()._attachments;
if (attachments) {
Expand Down
25 changes: 7 additions & 18 deletions src/app/users/users-achievements/users-achievements.scss
Original file line number Diff line number Diff line change
Expand Up @@ -123,40 +123,29 @@
min-height: 900px;
}

.auto-adjust-buttons {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.25rem;
}

.margin-r-1 {
margin-right: 1rem
}

@media (max-width: v.$screen-sm) {
.responsive-toolbar {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
height: auto;
padding-bottom: 0.5rem;
}

.auto-adjust-buttons {
display: flex;
gap: 0.2rem;
width: 100%;
}

.auto-adjust-buttons a {
flex-grow: 1;
text-align: center;
}

.auto-adjust-buttons span {
font-size: 0.875em;
}

.margin-r-1 {
margin-right: 0.2rem;
}

.center-text div {
margin: 0 auto;
}
}
Loading