diff --git a/package.json b/package.json index 5d05b86e8..5cefd7db4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vernite", - "version": "1.0.0", + "version": "1.0.1", "scripts": { "@------ WORKFLOW SCRIPTS --------": "", "postinstall": "husky install", diff --git a/src/app/_main/components/sidebar-navigation/sidebar-navigation.component.html b/src/app/_main/components/sidebar-navigation/sidebar-navigation.component.html index 40a94d8d2..615758a29 100644 --- a/src/app/_main/components/sidebar-navigation/sidebar-navigation.component.html +++ b/src/app/_main/components/sidebar-navigation/sidebar-navigation.component.html @@ -12,7 +12,12 @@ Calendar - + + Messages + + Messages
- + + Workspaces + + + + + + + Workspaces
- - {{ workspace.name }} -
- - {{ projectWithPrivileges.project.name }} - - - - - - - - -
- - - - - - -
+ + + {{ workspace.name }} + + + + + + + + + + {{ workspace.name }} +
+ + {{ projectWithPrivileges.project.name }} + + + + + + + + +
+ + + + + + +
+
diff --git a/src/app/_main/dialogs/report-bug/report-bug.dialog.ts b/src/app/_main/dialogs/report-bug/report-bug.dialog.ts index 57a0a4a65..c89cbb7cd 100644 --- a/src/app/_main/dialogs/report-bug/report-bug.dialog.ts +++ b/src/app/_main/dialogs/report-bug/report-bug.dialog.ts @@ -3,6 +3,7 @@ import { Component, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@ngneat/reactive-forms'; import { validateForm } from '@main/classes/form.class'; import { requiredValidator } from '@main/validators/required.validator'; +import { lengthValidator } from '@main/validators/length.validator'; /** Report bug dialog component */ @Component({ @@ -13,8 +14,11 @@ import { requiredValidator } from '@main/validators/required.validator'; export class ReportBugDialog implements OnInit { /** Report bug dialog form */ public form = new FormGroup({ - title: new FormControl('', [requiredValidator()]), - description: new FormControl(''), + title: new FormControl('', [requiredValidator(), lengthValidator(3, 100)]), + description: new FormControl('', [ + requiredValidator(), + lengthValidator(3, 1000), + ]), }); constructor(private dialogRef: MatDialogRef) {} diff --git a/src/app/_main/modules/audit-log/components/audit-log/audit-log.component.html b/src/app/_main/modules/audit-log/components/audit-log/audit-log.component.html index c9dee8f99..da1d209f8 100644 --- a/src/app/_main/modules/audit-log/components/audit-log/audit-log.component.html +++ b/src/app/_main/modules/audit-log/components/audit-log/audit-log.component.html @@ -1,4 +1,9 @@ - - - + + + + + + + + diff --git a/src/app/_main/modules/audit-log/components/audit-log/audit-log.component.ts b/src/app/_main/modules/audit-log/components/audit-log/audit-log.component.ts index c363395f6..9c780c4c2 100644 --- a/src/app/_main/modules/audit-log/components/audit-log/audit-log.component.ts +++ b/src/app/_main/modules/audit-log/components/audit-log/audit-log.component.ts @@ -9,6 +9,8 @@ import { AuditLog } from '../../interfaces/audit-log.interface'; export class AuditLogComponent { @Input() auditLogs!: AuditLog[]; + @Input() noScroll: boolean = false; + public trackByAuditLog(index: number, auditLog: AuditLog) { return auditLog.date; } diff --git a/src/app/_main/modules/audit-log/pipes/audit-log-project-entries/audit-log-project-entries.pipe.ts b/src/app/_main/modules/audit-log/pipes/audit-log-project-entries/audit-log-project-entries.pipe.ts index 74c20a4d2..1b8eec3f6 100644 --- a/src/app/_main/modules/audit-log/pipes/audit-log-project-entries/audit-log-project-entries.pipe.ts +++ b/src/app/_main/modules/audit-log/pipes/audit-log-project-entries/audit-log-project-entries.pipe.ts @@ -148,12 +148,20 @@ export class AuditLogProjectEntriesPipe implements PipeTransform { case 'assigneeId': return { label: $localize`assignee`, - oldValue: members.find( - (member) => member.user.id === auditLog.oldValues?.assigneeId, - )?.user.name, - newValue: members.find( - (member) => member.user.id === auditLog.newValues?.assigneeId, - )?.user.name, + oldValue: (() => { + const member = members.find( + (member) => member.user.id === auditLog.oldValues?.assigneeId, + ); + if (!member) + return $localize`:unknown user|ctx. Changed assignee from unknown user to someone else:Unknown`; + return `${member?.user.name} ${member?.user.surname}`; + })(), + newValue: (() => { + const member = members.find( + (member) => member.user.id === auditLog.newValues?.assigneeId, + ); + return `${member?.user.name} ${member?.user.surname}`; + })(), }; case 'sprintId': return { diff --git a/src/app/_main/validators/boolean.validator.spec.ts b/src/app/_main/validators/boolean.validator.spec.ts new file mode 100644 index 000000000..1ce0eb308 --- /dev/null +++ b/src/app/_main/validators/boolean.validator.spec.ts @@ -0,0 +1,22 @@ +import { FormControl } from '@ngneat/reactive-forms'; +import { expectToFail, expectToPass } from '@tests/helpers/validator-testing.helper'; +import { booleanValidator } from './boolean.validator'; + +describe('Test boolean validator', () => { + const validator = booleanValidator(); + + it('should pass', () => { + const control = new FormControl(true); + expectToPass(validator(control)); + }); + + it('should pass', () => { + const control = new FormControl(false); + expectToPass(validator(control)); + }); + + it('should not pass', () => { + const control = new FormControl(null); + expectToFail(validator(control)); + }); +}); diff --git a/src/app/_main/validators/boolean.validator.ts b/src/app/_main/validators/boolean.validator.ts new file mode 100644 index 000000000..80127019e --- /dev/null +++ b/src/app/_main/validators/boolean.validator.ts @@ -0,0 +1,18 @@ +import { AbstractControl, ValidatorFn } from '@angular/forms'; +import { ValidationError } from '../interfaces/validation-error.interface'; + +/** + * Validator to check if the variable is a boolean + * @returns {ValidatorFn} + */ +export function booleanValidator(): ValidatorFn { + return (control: AbstractControl): ValidationError | null => { + if (control.value !== true && control.value !== false) { + return { + type: 'boolean', + message: $localize`It should be a boolean`, + }; + } + return null; + }; +} diff --git a/src/app/_main/validators/length.validator.spec.ts b/src/app/_main/validators/length.validator.spec.ts new file mode 100644 index 000000000..bd6f48a9e --- /dev/null +++ b/src/app/_main/validators/length.validator.spec.ts @@ -0,0 +1,32 @@ +import { FormControl } from '@ngneat/reactive-forms'; +import { expectToFail, expectToPass } from '@tests/helpers/validator-testing.helper'; +import { lengthValidator } from './length.validator'; + +describe('Test length limit validator', () => { + const validator = lengthValidator(3, 50); + + it('should pass', () => { + const control = new FormControl('a0123456789'); + expectToPass(validator(control)); + }); + + it('should pass', () => { + const control = new FormControl('abc'); + expectToPass(validator(control)); + }); + + it('should pass', () => { + const control = new FormControl(''); + expectToPass(validator(control)); + }); + + it('should not pass', () => { + const control = new FormControl('a'); + expectToFail(validator(control)); + }); + + it('should not pass', () => { + const control = new FormControl('012345678901234567890123456789012345678901234567890'); + expectToFail(validator(control)); + }); +}); diff --git a/src/app/_main/validators/length.validator.ts b/src/app/_main/validators/length.validator.ts new file mode 100644 index 000000000..256ffb7d1 --- /dev/null +++ b/src/app/_main/validators/length.validator.ts @@ -0,0 +1,23 @@ +import { AbstractControl, ValidatorFn } from '@angular/forms'; +import { ValidationError } from '../interfaces/validation-error.interface'; + +/** + * Validator to check if the length is not lesser or greater than specyfic numbers, but is different than 0 + * @returns {ValidatorFn} + */ +export function lengthValidator(min_length: number, max_length: number): ValidatorFn { + return (control: AbstractControl): ValidationError | null => { + if (control.value.length > max_length) { + return { + type: 'max-length', + message: $localize`Length should not be greater than ${max_length}`, + }; + } else if (control.value.length < min_length && control.value.length != 0) { + return { + type: 'min-length', + message: $localize`Length should not be lesser than ${min_length}`, + }; + } + return null; + }; +} diff --git a/src/app/_main/validators/max-length.validator.spec.ts b/src/app/_main/validators/max-length.validator.spec.ts index 5385141c1..db981f6a4 100644 --- a/src/app/_main/validators/max-length.validator.spec.ts +++ b/src/app/_main/validators/max-length.validator.spec.ts @@ -1,16 +1,17 @@ import { FormControl } from '@ngneat/reactive-forms'; +import { expectToFail, expectToPass } from '@tests/helpers/validator-testing.helper'; import { maxLengthValidator } from './max-length.validator'; -describe('Test length limit validator', () => { +describe('Test maximum length limit validator', () => { const validator = maxLengthValidator(50); it('should pass', () => { const control = new FormControl('a0123456789'); - expect(validator(control)).toBeNull(); + expectToPass(validator(control)); }); it('should not pass', () => { const control = new FormControl('012345678901234567890123456789012345678901234567890'); - expect(validator(control)).toBeTruthy(); + expectToFail(validator(control)); }); }); diff --git a/src/app/_main/validators/min-length.validator.spec.ts b/src/app/_main/validators/min-length.validator.spec.ts new file mode 100644 index 000000000..d8cd94c71 --- /dev/null +++ b/src/app/_main/validators/min-length.validator.spec.ts @@ -0,0 +1,22 @@ +import { FormControl } from '@ngneat/reactive-forms'; +import { expectToFail, expectToPass } from '@tests/helpers/validator-testing.helper'; +import { minLengthValidator } from './min-length.validator'; + +describe('Test minimum length limit validator', () => { + const validator = minLengthValidator(3); + + it('should pass', () => { + const control = new FormControl('abc'); + expectToPass(validator(control)); + }); + + it('should pass', () => { + const control = new FormControl(''); + expectToPass(validator(control)); + }); + + it('should not pass', () => { + const control = new FormControl('a'); + expectToFail(validator(control)); + }); +}); diff --git a/src/app/_main/validators/min-length.validator.ts b/src/app/_main/validators/min-length.validator.ts new file mode 100644 index 000000000..a13b753bc --- /dev/null +++ b/src/app/_main/validators/min-length.validator.ts @@ -0,0 +1,18 @@ +import { AbstractControl, ValidatorFn } from '@angular/forms'; +import { ValidationError } from '../interfaces/validation-error.interface'; + +/** + * Validator to check if the length is not lesser than specyfic number, but is different than 0 + * @returns {ValidatorFn} + */ +export function minLengthValidator(length: number): ValidatorFn { + return (control: AbstractControl): ValidationError | null => { + if (control.value.length < length && control.value.length != 0) { + return { + type: 'min-length', + message: $localize`Length should not be lesser than ${length}`, + }; + } + return null; + }; +} diff --git a/src/app/_main/validators/not-empty.validator.ts b/src/app/_main/validators/not-empty.validator.ts index 75a5e6aeb..859a058f9 100644 --- a/src/app/_main/validators/not-empty.validator.ts +++ b/src/app/_main/validators/not-empty.validator.ts @@ -8,7 +8,10 @@ import { ValidationError } from '../interfaces/validation-error.interface'; export function notEmptyValidator(): ValidatorFn { return function notEmpty(control: AbstractControl): ValidationError | null { if (control.value && control.value.trim() === '') { - return { type: 'not-empty', message: $localize`This field cannot consist of only spaces` }; + return { + type: 'not-empty', + message: $localize`This field requires a non-whitespace value`, + }; } return null; }; diff --git a/src/app/_main/validators/not-negative-number.validator.spec.ts b/src/app/_main/validators/not-negative-number.validator.spec.ts new file mode 100644 index 000000000..0c2a33d64 --- /dev/null +++ b/src/app/_main/validators/not-negative-number.validator.spec.ts @@ -0,0 +1,17 @@ +import { FormControl } from '@ngneat/reactive-forms'; +import { expectToFail, expectToPass } from '@tests/helpers/validator-testing.helper'; +import { notNegativeNumberValidator } from './not-negative-number.validator'; + +describe('Test non negative number validator', () => { + const validator = notNegativeNumberValidator(); + + it('should pass', () => { + const control = new FormControl(10); + expectToPass(validator(control)); + }); + + it('should not pass', () => { + const control = new FormControl(-1); + expectToFail(validator(control)); + }); +}); diff --git a/src/app/_main/validators/not-negative-number.validator.ts b/src/app/_main/validators/not-negative-number.validator.ts new file mode 100644 index 000000000..168274dba --- /dev/null +++ b/src/app/_main/validators/not-negative-number.validator.ts @@ -0,0 +1,18 @@ +import { AbstractControl, ValidatorFn } from '@angular/forms'; +import { ValidationError } from '../interfaces/validation-error.interface'; + +/** + * Validator to check if the number is not negative + * @returns {ValidatorFn} + */ +export function notNegativeNumberValidator(): ValidatorFn { + return (control: AbstractControl): ValidationError | null => { + if (control.value < 0) { + return { + type: 'not-negative-number', + message: $localize`Number should not be negative`, + }; + } + return null; + }; +} diff --git a/src/app/_main/validators/same-as.validator.spec.ts b/src/app/_main/validators/same-as.validator.spec.ts index f994ce971..7401baa9f 100644 --- a/src/app/_main/validators/same-as.validator.spec.ts +++ b/src/app/_main/validators/same-as.validator.spec.ts @@ -2,21 +2,21 @@ import { FormControl, FormGroup } from '@ngneat/reactive-forms'; import { sameAsValidator } from './same-as.validator'; describe('Test if two values are the same', () => { - // it('should be valid if passwords are the same', () => { - // const form = new FormGroup({ - // password: new FormControl('abcd1234', [], []), - // repeatPassword: new FormControl('abcd1234', [sameAsValidator('password', ``)], []), - // }); - // form.updateValueAndValidity(); - // expect(form.valid).toBeTruthy(); - // }); + it('should be valid if passwords are the same', () => { + const form = new FormGroup({ + password: new FormControl('abcd1234', []), + repeatPassword: new FormControl('abcd1234', [sameAsValidator('password', ``)]), + }); + form.get('repeatPassword').updateValueAndValidity(); + expect(form.valid).toBeTruthy(); + }); it('should throw error if passwords are different', () => { const form = new FormGroup({ - password: new FormControl('1234abcd', [], []), - repeatPassword: new FormControl('abcd1234', [sameAsValidator('password', ``)], []), + password: new FormControl('1234abcd', []), + repeatPassword: new FormControl('abcd1234', [sameAsValidator('password', ``)]), }); - form.updateValueAndValidity(); + form.get('repeatPassword').updateValueAndValidity(); expect(form.valid).toBeFalse(); }); }); diff --git a/src/app/auth/pages/change-password/change-password.page.ts b/src/app/auth/pages/change-password/change-password.page.ts index 78e2cdb67..b15ebd4b4 100644 --- a/src/app/auth/pages/change-password/change-password.page.ts +++ b/src/app/auth/pages/change-password/change-password.page.ts @@ -31,16 +31,12 @@ export class ChangePasswordPage implements OnInit { * Form group for setting new password. */ public form = new FormGroup({ - password: new FormControl('', [requiredValidator(), passwordValidator()], []), - repeatPassword: new FormControl( - '', - [ - requiredValidator(), - passwordValidator(), - sameAsValidator('password', $localize`Given passwords are not the same `), - ], - [], - ), + password: new FormControl('', [requiredValidator(), passwordValidator()]), + repeatPassword: new FormControl('', [ + requiredValidator(), + passwordValidator(), + sameAsValidator('password', $localize`Given passwords are not the same `), + ]), }); constructor( @@ -65,7 +61,7 @@ export class ChangePasswordPage implements OnInit { if (this.form.valid && this.token) { this.resetSubscription = this.authService - .setNewPassword(this.token, this.form.value.password) + .setNewPasswordFromToken(this.token, this.form.value.password) .subscribe(() => { this.router.navigate(['/auth/login']); }); diff --git a/src/app/auth/pages/delete-account/delete-account.page.ts b/src/app/auth/pages/delete-account/delete-account.page.ts index 8dd6e083c..a206814ff 100644 --- a/src/app/auth/pages/delete-account/delete-account.page.ts +++ b/src/app/auth/pages/delete-account/delete-account.page.ts @@ -28,7 +28,7 @@ export class DeleteAccountPage implements OnInit { */ deleteAccount(token: string) { if (token) { - this.authService.deleteAccountConfirmation(token).subscribe(() => {}); + this.authService.deleteAccount(token).subscribe(() => {}); } } } diff --git a/src/app/auth/pages/forgot-password/forgot-password.page.ts b/src/app/auth/pages/forgot-password/forgot-password.page.ts index dab9859a7..cb09f2c1a 100644 --- a/src/app/auth/pages/forgot-password/forgot-password.page.ts +++ b/src/app/auth/pages/forgot-password/forgot-password.page.ts @@ -5,6 +5,7 @@ import { faAngleDown } from '@fortawesome/free-solid-svg-icons'; import { requiredValidator } from '@main/validators/required.validator'; import { Subscription } from 'rxjs'; import { AuthService } from '@auth/services/auth/auth.service'; +import { emailValidator } from '@main/validators/email.validator'; /** * Forgot password page component @@ -30,7 +31,7 @@ export class ForgotPasswordPage { * Form group for reset password. */ public form = new FormGroup({ - email: new FormControl('', [requiredValidator()], []), + email: new FormControl('', [requiredValidator(), emailValidator()]), }); /** diff --git a/src/app/auth/pages/login/login.page.ts b/src/app/auth/pages/login/login.page.ts index 00ad26740..322ec7d9f 100644 --- a/src/app/auth/pages/login/login.page.ts +++ b/src/app/auth/pages/login/login.page.ts @@ -6,6 +6,7 @@ import { requiredValidator } from 'src/app/_main/validators/required.validator'; import { AuthService } from '@auth/services/auth/auth.service'; import { Loader } from '../../../_main/classes/loader/loader.class'; import { startLoader, stopLoader } from '../../../_main/operators/loader.operator'; +import { booleanValidator } from '@main/validators/boolean.validator'; /** * Login page component @@ -35,9 +36,9 @@ export class LoginPage implements OnInit { /** Form group for login. */ public form = new FormGroup({ - email: new FormControl('', [requiredValidator()], []), - password: new FormControl('', [requiredValidator()], []), - remember: new FormControl(false, [], []), + email: new FormControl('', [requiredValidator()]), + password: new FormControl('', [requiredValidator()]), + remember: new FormControl(false, [booleanValidator()]), }); ngOnInit() { diff --git a/src/app/auth/pages/register/register.page.ts b/src/app/auth/pages/register/register.page.ts index 7275dc386..5cc5f3ed0 100644 --- a/src/app/auth/pages/register/register.page.ts +++ b/src/app/auth/pages/register/register.page.ts @@ -10,6 +10,7 @@ import { AuthService } from '@auth/services/auth/auth.service'; import { Loader } from '../../../_main/classes/loader/loader.class'; import { startLoader, stopLoader } from '../../../_main/operators/loader.operator'; import { ReCaptchaV3Service } from 'ng-recaptcha'; +import { booleanValidator } from '@main/validators/boolean.validator'; /** * Register stages @@ -58,12 +59,12 @@ export class RegisterPage { repeatPassword: new FormControl('', [ requiredValidator(), passwordValidator(), - sameAsValidator('password', $localize`Given passwords are not the same `), + sameAsValidator('password', $localize`Given passwords are not the same`), ]), name: new FormControl('', [requiredValidator()]), surname: new FormControl('', [requiredValidator()]), username: new FormControl('', [requiredValidator()]), - agreements: new FormControl('', [requiredValidator()]), + agreements: new FormControl('', [requiredValidator(), booleanValidator()]), }); /** diff --git a/src/app/auth/services/auth/auth.service.spec.ts b/src/app/auth/services/auth/auth.service.spec.ts index 6d7caa761..f36bff3ae 100644 --- a/src/app/auth/services/auth/auth.service.spec.ts +++ b/src/app/auth/services/auth/auth.service.spec.ts @@ -3,15 +3,19 @@ import { HttpClientModule } from '@angular/common/http'; import { TestBed, inject } from '@angular/core/testing'; import { AuthService } from './auth.service'; +import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { RECAPTCHA_V3_SITE_KEY, ReCaptchaV3Service } from 'ng-recaptcha'; import { environment } from 'src/environments/environment'; describe('Service: AuthService', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientModule], + imports: [HttpClientModule, MatDialogModule], providers: [ AuthService, + MatDialogModule, + { provide: MAT_DIALOG_DATA, useValue: {} }, + { provide: MatDialogRef, useValue: {} }, { provide: RECAPTCHA_V3_SITE_KEY, useValue: environment.captchaSiteKey }, { provide: ReCaptchaV3Service, useClass: ReCaptchaV3Service }, ], diff --git a/src/app/auth/services/auth/auth.service.ts b/src/app/auth/services/auth/auth.service.ts index 720853ae8..6a49e87c7 100644 --- a/src/app/auth/services/auth/auth.service.ts +++ b/src/app/auth/services/auth/auth.service.ts @@ -1,13 +1,16 @@ import { Injectable, Injector } from '@angular/core'; import { Service } from '@main/decorators/service/service.decorator'; import dayjs from 'dayjs'; -import { tap, Observable, switchMap, catchError, map, of } from 'rxjs'; +import { tap, Observable, switchMap, catchError, map, of, EMPTY } from 'rxjs'; import { ApiService } from '@main/services/api/api.service'; import { BaseService } from '@main/services/base/base.service'; import { Errors } from '@main/interfaces/http-error.interface'; import { User } from '../../interfaces/user.interface'; import { ReCaptchaV3Service } from 'ng-recaptcha'; import { ProtoService } from '@main/services/proto/proto.service'; +import { AlertDialogVariant } from '@main/dialogs/alert/alert.dialog'; +import { DialogService } from '@main/services/dialog/dialog.service'; +import { ChangePasswordDialog } from 'src/app/settings/dialog/change-password/change-password.dialog'; /** * Authentication service @@ -28,6 +31,7 @@ export class AuthService extends BaseService> { private apiService: ApiService, private recaptchaV3Service: ReCaptchaV3Service, private protoService: ProtoService, + private dialogService: DialogService, ) { super(injector); } @@ -116,24 +120,56 @@ export class AuthService extends BaseService> { * @param password new password * @returns set new password response */ - public setNewPassword(token: string, password: string) { + public setNewPasswordFromToken(token: string, password: string) { return this.apiService.post(`/auth/password/reset`, { body: { token, password } }); } + public changePassword(oldPassword: string, newPassword: string) { + return this.apiService.post(`/auth/password/change`, { body: { oldPassword, newPassword } }); + } + + public openChangePasswordDialog() { + return this.dialogService.open(ChangePasswordDialog, {}).afterClosed(); + } + /** * Delete account * @returns delete account response */ - public deleteAccount() { + public sendEmailToDeleteAccount() { return this.apiService.delete(`/auth/delete`); } + /** + * Delete account with confirmation + */ + public deleteAccountWithConfirmation(): Observable { + return this.dialogService + .confirm({ + title: $localize`Delete account`, + message: $localize`Upon proceeding, an email with a deletion link will be send to your email account. To delete account you will need to click given link. Are you sure you want to proceed with deleting your account? Please note that this action will log you out.`, + confirmText: $localize`Delete`, + cancelText: $localize`Cancel`, + variant: AlertDialogVariant.IMPORTANT, + }) + .pipe( + switchMap((confirmed) => { + if (!confirmed) return EMPTY; + return this.sendEmailToDeleteAccount().pipe( + switchMap(() => { + return this.logout(); + }), + ); + }), + ); + } + /** * Delete account confirmation * @param token delete account token received by email * @returns delete account confirmation response */ - public deleteAccountConfirmation(token: string) { + public deleteAccount(token: string) { return this.apiService.delete(`/auth/delete/confirm`, { body: { token } }).pipe( this.validate({ 403: 'INVALID_TOKEN', diff --git a/src/app/auth/services/user/user.service.spec.ts b/src/app/auth/services/user/user.service.spec.ts index bf3df9211..8c4008391 100644 --- a/src/app/auth/services/user/user.service.spec.ts +++ b/src/app/auth/services/user/user.service.spec.ts @@ -4,12 +4,13 @@ import { HttpClientModule } from '@angular/common/http'; import { inject, TestBed } from '@angular/core/testing'; import { UserService } from './user.service'; import { RECAPTCHA_V3_SITE_KEY, ReCaptchaV3Service } from 'ng-recaptcha'; +import { MatDialogModule } from '@angular/material/dialog'; import { environment } from '../../../../environments/environment'; describe('Service: User', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientModule], + imports: [HttpClientModule, MatDialogModule], providers: [ UserService, { provide: RECAPTCHA_V3_SITE_KEY, useValue: environment.captchaSiteKey }, diff --git a/src/app/calendar/dialogs/meeting/meeting.dialog.ts b/src/app/calendar/dialogs/meeting/meeting.dialog.ts index 6c70e6d7b..fdd459403 100644 --- a/src/app/calendar/dialogs/meeting/meeting.dialog.ts +++ b/src/app/calendar/dialogs/meeting/meeting.dialog.ts @@ -11,6 +11,9 @@ import { MemberService } from '@dashboard/services/member/member.service'; import { UserService } from '@auth/services/user/user.service'; import { Loader } from '../../../_main/classes/loader/loader.class'; import { withLoader } from '@main/operators/loader.operator'; +import { maxLengthValidator } from '@main/validators/max-length.validator'; +import { lengthValidator } from '@main/validators/length.validator'; +import { notEmptyValidator } from '@main/validators/not-empty.validator'; /** Meeting dialog variant */ export enum MeetingDialogVariant { @@ -51,9 +54,16 @@ export class MeetingDialog implements OnInit { public form = new FormGroup({ id: new FormControl(null), projectId: new FormControl(null, [requiredValidator()]), - name: new FormControl('', [requiredValidator()]), - location: new FormControl(''), - description: new FormControl(''), + name: new FormControl('', [ + requiredValidator(), + lengthValidator(1, 50), + notEmptyValidator(), + ]), + description: new FormControl('', [ + maxLengthValidator(1000), + notEmptyValidator(), + ]), + location: new FormControl('', [maxLengthValidator(1000)]), startDate: new FormControl(null, [requiredValidator()]), endDate: new FormControl(null, [requiredValidator()]), participantIds: new FormControl(null), diff --git a/src/app/dashboard/components/project-form-general/project-form-general.component.ts b/src/app/dashboard/components/project-form-general/project-form-general.component.ts index 3b212c1fa..b8fbca693 100644 --- a/src/app/dashboard/components/project-form-general/project-form-general.component.ts +++ b/src/app/dashboard/components/project-form-general/project-form-general.component.ts @@ -13,6 +13,7 @@ import { omit } from 'lodash-es'; import { ApiFile } from '@main/interfaces/api-file.interface'; import { isApiFile } from '@main/util/is-api-file/is-api-file.util'; import { ProjectForm } from '../../interfaces/project-form.interface'; +import { lengthValidator } from '@main/validators/length.validator'; /** * Project form general component @@ -42,9 +43,9 @@ export class ProjectFormGeneralComponent implements OnInit, ProjectForm { name: new FormControl('', [ requiredValidator(), notEmptyValidator(), - maxLengthValidator(50), + lengthValidator(1, 50), ]), - description: new FormControl(''), + description: new FormControl('', [maxLengthValidator(1000)]), logo: new FormControl(undefined), }); diff --git a/src/app/dashboard/dashboard.module.ts b/src/app/dashboard/dashboard.module.ts index 01cab6928..93a1e21bd 100644 --- a/src/app/dashboard/dashboard.module.ts +++ b/src/app/dashboard/dashboard.module.ts @@ -14,6 +14,8 @@ import { DashboardRoutingModule } from './dashboard.routing'; import { AddMemberDialog } from './dialogs/add-member/add-member.dialog'; import { StatusDialog } from './dialogs/status/status.dialog'; import { IntegrationModulesModule } from './modules/integration-modules/integration-modules.module'; +import { WidgetContentComponent } from './modules/widgets/components/widget-content/widget-content.component'; +import { WidgetHeaderComponent } from './modules/widgets/components/widget-header/widget-header.component'; import { CreateProjectPage } from './pages/create-project/create-project.page'; import { CreateWorkspacePage } from './pages/create-workspace/create-workspace.page'; import { EditProjectPage } from './pages/edit-project/edit-project.page'; @@ -22,6 +24,10 @@ import { ProjectPage } from './pages/project/project.page'; import { ProjectsListPage } from './pages/projects-list/projects-list.page'; import { WorkspacesListPage } from './pages/workspaces-list/workspaces-list.page'; import { GitIntegrationService } from './services/git-integration/git-integration.service'; +import { WidgetTasksComponent } from './modules/widgets/components/widget-tasks/widget-tasks.component'; +import { DashboardPage } from './pages/dashboard/dashboard.page'; +import { WidgetAuditLogComponent } from './modules/widgets/components/widget-audit-log/widget-audit-log.component'; +import { AuditLogModule } from '../_main/modules/audit-log/audit-log.module'; @NgModule({ imports: [ @@ -31,6 +37,7 @@ import { GitIntegrationService } from './services/git-integration/git-integratio DashboardRoutingModule, IntegrationModulesModule, TasksModule, + AuditLogModule, ], declarations: [ CreateWorkspacePage, @@ -49,6 +56,11 @@ import { GitIntegrationService } from './services/git-integration/git-integratio AddMemberDialog, IntegrationModulesGridComponent, StatusDialog, + WidgetContentComponent, + WidgetHeaderComponent, + WidgetTasksComponent, + WidgetAuditLogComponent, + DashboardPage, ], providers: [GitIntegrationService], }) diff --git a/src/app/dashboard/dashboard.routing.ts b/src/app/dashboard/dashboard.routing.ts index a982cc7ed..e791fdc0e 100644 --- a/src/app/dashboard/dashboard.routing.ts +++ b/src/app/dashboard/dashboard.routing.ts @@ -9,6 +9,7 @@ import { ProjectsListPage } from './pages/projects-list/projects-list.page'; import { WorkspacesListPage } from './pages/workspaces-list/workspaces-list.page'; import { CreateProjectPage } from './pages/create-project/create-project.page'; import { ProjectPage } from './pages/project/project.page'; +import { DashboardPage } from './pages/dashboard/dashboard.page'; /** * Dashboard routes list @@ -27,10 +28,7 @@ const routes: Routes = [ }, { path: 'dashboard', - component: MockPage, - data: { - image: 'assets/mocks/dashboard.svg', - }, + component: DashboardPage, }, // Workspaces sub route diff --git a/src/app/dashboard/dialogs/status/status.dialog.ts b/src/app/dashboard/dialogs/status/status.dialog.ts index ed41dc9e9..711e15e6d 100644 --- a/src/app/dashboard/dialogs/status/status.dialog.ts +++ b/src/app/dashboard/dialogs/status/status.dialog.ts @@ -5,6 +5,10 @@ import { requiredValidator } from '@main/validators/required.validator'; import { FormControl, FormGroup } from '@ngneat/reactive-forms'; import { Status } from '@tasks/interfaces/status.interface'; import { validateForm } from '@main/classes/form.class'; +import { lengthValidator } from '@main/validators/length.validator'; +import { notEmptyValidator } from '@main/validators/not-empty.validator'; +import { booleanValidator } from '@main/validators/boolean.validator'; +import { notNegativeNumberValidator } from '@main/validators/not-negative-number.validator'; /** * Status dialog data @@ -29,10 +33,10 @@ export interface StatusDialogData { export class StatusDialog implements OnInit { /** Status dialog form */ public form = new FormGroup({ - name: new FormControl('', [requiredValidator()]), - begin: new FormControl(false), - final: new FormControl(false), - color: new FormControl(0, [requiredValidator()]), + name: new FormControl('', [requiredValidator(), lengthValidator(1, 50), notEmptyValidator()]), + begin: new FormControl(false, [booleanValidator()]), + final: new FormControl(false, [booleanValidator()]), + color: new FormControl(0, [requiredValidator(), notNegativeNumberValidator()]), }); constructor( diff --git a/src/app/dashboard/modules/widgets/components/widget-audit-log/widget-audit-log.component.html b/src/app/dashboard/modules/widgets/components/widget-audit-log/widget-audit-log.component.html new file mode 100644 index 000000000..31e192b95 --- /dev/null +++ b/src/app/dashboard/modules/widgets/components/widget-audit-log/widget-audit-log.component.html @@ -0,0 +1,15 @@ + +

Changelog

+
+ +
+

{{ group.project.name }}

+ +
+
+

No updates were made to any of your projects.

+
+
+ +
+
diff --git a/src/app/dashboard/modules/widgets/components/widget-audit-log/widget-audit-log.component.scss b/src/app/dashboard/modules/widgets/components/widget-audit-log/widget-audit-log.component.scss new file mode 100644 index 000000000..82a0db8dc --- /dev/null +++ b/src/app/dashboard/modules/widgets/components/widget-audit-log/widget-audit-log.component.scss @@ -0,0 +1,18 @@ +.project { + padding: 1rem; + background-color: var(--color-secondary-800); + border-radius: var(--border-radius); + + p { + padding-bottom: 1rem; + } +} + +.loader, +.empty { + display: flex; + justify-content: center; + padding: 2rem; + background-color: var(--color-secondary-800); + border-radius: var(--border-radius); +} diff --git a/src/app/dashboard/modules/widgets/components/widget-audit-log/widget-audit-log.component.ts b/src/app/dashboard/modules/widgets/components/widget-audit-log/widget-audit-log.component.ts new file mode 100644 index 000000000..feae64ff2 --- /dev/null +++ b/src/app/dashboard/modules/widgets/components/widget-audit-log/widget-audit-log.component.ts @@ -0,0 +1,65 @@ +import { Component, OnInit } from '@angular/core'; +import { TaskService } from '../../../../../tasks/services/task/task.service'; +import { map, Observable, switchMap, forkJoin, of, EMPTY } from 'rxjs'; +import { MemberService } from '../../../../services/member/member.service'; +import { ProjectMember } from '../../../../interfaces/project-member.interface'; +import { Status } from '../../../../../tasks/interfaces/status.interface'; +import { Task } from '@tasks/interfaces/task.interface'; +import { ProjectService } from '../../../../services/project/project.service'; +import { StatusService } from '../../../../../tasks/services/status/status.service'; +import { Project } from '../../../../interfaces/project.interface'; +import { TaskFilters } from '../../../../../tasks/filters/task.filters'; +import { UserService } from '../../../../../auth/services/user/user.service'; +import { Loader } from '../../../../../_main/classes/loader/loader.class'; +import { withLoader } from '../../../../../_main/operators/loader.operator'; +import { AuditLog } from '../../../../../_main/modules/audit-log/interfaces/audit-log.interface'; + +@Component({ + selector: 'widget-audit-log', + templateUrl: './widget-audit-log.component.html', + styleUrls: ['./widget-audit-log.component.scss'], +}) +export class WidgetAuditLogComponent implements OnInit { + public projects$: Observable< + { + project: Project; + auditLog: AuditLog[]; + }[] + > = EMPTY; + + public columns: Set = new Set(['title', 'status', 'time-tracking']); + + public loader = new Loader(); + + constructor( + private taskService: TaskService, + private memberService: MemberService, + private projectService: ProjectService, + private statusService: StatusService, + private userService: UserService, + ) {} + + ngOnInit() { + this.projects$ = this.loadProjects().pipe(withLoader(this.loader)); + } + + private loadProjects() { + return this.projectService.list().pipe( + switchMap((projects) => { + return forkJoin(projects.map((project) => this.loadProject(project))); + }), + map((projects) => projects.filter((project) => project.auditLog.length > 0)), + ); + } + + private loadProject(project: Project) { + return this.userService.getMyself().pipe( + switchMap((user) => { + return forkJoin({ + project: of(project), + auditLog: this.projectService.auditLog(project.id), + }); + }), + ); + } +} diff --git a/src/app/dashboard/modules/widgets/components/widget-content/widget-content.component.html b/src/app/dashboard/modules/widgets/components/widget-content/widget-content.component.html new file mode 100644 index 000000000..6dbc74306 --- /dev/null +++ b/src/app/dashboard/modules/widgets/components/widget-content/widget-content.component.html @@ -0,0 +1 @@ + diff --git a/src/app/dashboard/modules/widgets/components/widget-content/widget-content.component.scss b/src/app/dashboard/modules/widgets/components/widget-content/widget-content.component.scss new file mode 100644 index 000000000..9854b359c --- /dev/null +++ b/src/app/dashboard/modules/widgets/components/widget-content/widget-content.component.scss @@ -0,0 +1,5 @@ +:host { + display: block; + width: 100%; + padding: 1rem; +} diff --git a/src/app/dashboard/modules/widgets/components/widget-content/widget-content.component.ts b/src/app/dashboard/modules/widgets/components/widget-content/widget-content.component.ts new file mode 100644 index 000000000..45ba1e4bd --- /dev/null +++ b/src/app/dashboard/modules/widgets/components/widget-content/widget-content.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'widget-content', + templateUrl: './widget-content.component.html', + styleUrls: ['./widget-content.component.scss'], +}) +export class WidgetContentComponent {} diff --git a/src/app/dashboard/modules/widgets/components/widget-header/widget-header.component.html b/src/app/dashboard/modules/widgets/components/widget-header/widget-header.component.html new file mode 100644 index 000000000..df10cf8ee --- /dev/null +++ b/src/app/dashboard/modules/widgets/components/widget-header/widget-header.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/app/dashboard/modules/widgets/components/widget-header/widget-header.component.scss b/src/app/dashboard/modules/widgets/components/widget-header/widget-header.component.scss new file mode 100644 index 000000000..384c430a7 --- /dev/null +++ b/src/app/dashboard/modules/widgets/components/widget-header/widget-header.component.scss @@ -0,0 +1,11 @@ +:host { + display: block; + width: 100%; + padding: 1rem 1rem 0 1rem; + border-radius: var(--border-radius); +} + +.header-wrapper { + padding: 1rem; + background-color: var(--color-secondary-800); +} diff --git a/src/app/dashboard/modules/widgets/components/widget-header/widget-header.component.ts b/src/app/dashboard/modules/widgets/components/widget-header/widget-header.component.ts new file mode 100644 index 000000000..545c5833b --- /dev/null +++ b/src/app/dashboard/modules/widgets/components/widget-header/widget-header.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'widget-header', + templateUrl: './widget-header.component.html', + styleUrls: ['./widget-header.component.scss'], +}) +export class WidgetHeaderComponent { + constructor() {} +} diff --git a/src/app/dashboard/modules/widgets/components/widget-tasks/widget-tasks.component.html b/src/app/dashboard/modules/widgets/components/widget-tasks/widget-tasks.component.html new file mode 100644 index 000000000..96735b13b --- /dev/null +++ b/src/app/dashboard/modules/widgets/components/widget-tasks/widget-tasks.component.html @@ -0,0 +1,16 @@ + +

Tasks assigned to you

+
+ +
+

{{ group.project.name }}

+ +
+
+

No tasks have been assigned to you at the moment. Feel free to check back later to see if any new tasks have been added.

+
+
+ +
+
diff --git a/src/app/dashboard/modules/widgets/components/widget-tasks/widget-tasks.component.scss b/src/app/dashboard/modules/widgets/components/widget-tasks/widget-tasks.component.scss new file mode 100644 index 000000000..82a0db8dc --- /dev/null +++ b/src/app/dashboard/modules/widgets/components/widget-tasks/widget-tasks.component.scss @@ -0,0 +1,18 @@ +.project { + padding: 1rem; + background-color: var(--color-secondary-800); + border-radius: var(--border-radius); + + p { + padding-bottom: 1rem; + } +} + +.loader, +.empty { + display: flex; + justify-content: center; + padding: 2rem; + background-color: var(--color-secondary-800); + border-radius: var(--border-radius); +} diff --git a/src/app/dashboard/modules/widgets/components/widget-tasks/widget-tasks.component.ts b/src/app/dashboard/modules/widgets/components/widget-tasks/widget-tasks.component.ts new file mode 100644 index 000000000..d4eed4ecf --- /dev/null +++ b/src/app/dashboard/modules/widgets/components/widget-tasks/widget-tasks.component.ts @@ -0,0 +1,68 @@ +import { Component, OnInit } from '@angular/core'; +import { TaskService } from '../../../../../tasks/services/task/task.service'; +import { map, Observable, switchMap, forkJoin, of, EMPTY } from 'rxjs'; +import { MemberService } from '../../../../services/member/member.service'; +import { ProjectMember } from '../../../../interfaces/project-member.interface'; +import { Status } from '../../../../../tasks/interfaces/status.interface'; +import { Task } from '@tasks/interfaces/task.interface'; +import { ProjectService } from '../../../../services/project/project.service'; +import { StatusService } from '../../../../../tasks/services/status/status.service'; +import { Project } from '../../../../interfaces/project.interface'; +import { TaskFilters } from '../../../../../tasks/filters/task.filters'; +import { UserService } from '../../../../../auth/services/user/user.service'; +import { Loader } from '../../../../../_main/classes/loader/loader.class'; +import { withLoader } from '../../../../../_main/operators/loader.operator'; + +@Component({ + selector: 'widget-tasks', + templateUrl: './widget-tasks.component.html', + styleUrls: ['./widget-tasks.component.scss'], +}) +export class WidgetTasksComponent implements OnInit { + public projects$: Observable< + { + project: Project; + members: Map; + statuses: Status[]; + tasks: Task[]; + }[] + > = EMPTY; + + public columns: Set = new Set(['title', 'status', 'time-tracking']); + + public loader = new Loader(); + + constructor( + private taskService: TaskService, + private memberService: MemberService, + private projectService: ProjectService, + private statusService: StatusService, + private userService: UserService, + ) {} + + ngOnInit() { + this.projects$ = this.loadProjects().pipe(withLoader(this.loader)); + } + + private loadProjects() { + return this.projectService.list().pipe( + switchMap((projects) => { + return forkJoin(projects.map((project) => this.loadProject(project))); + }), + map((projects) => projects.filter((project) => project.tasks.length > 0)), + ); + } + + private loadProject(project: Project) { + return this.userService.getMyself().pipe( + switchMap((user) => { + return forkJoin({ + project: of(project), + members: this.memberService.map(project.id), + statuses: this.statusService.list(project.id), + tasks: this.taskService.list(project.id, TaskFilters.ASSIGNEE_ID(user.id)), + }); + }), + ); + } +} diff --git a/src/app/dashboard/pages/create-project-members/create-project-members.page.html b/src/app/dashboard/pages/create-project-members/create-project-members.page.html deleted file mode 100644 index 648649b51..000000000 --- a/src/app/dashboard/pages/create-project-members/create-project-members.page.html +++ /dev/null @@ -1,5 +0,0 @@ -
- Add member -
- - diff --git a/src/app/dashboard/pages/create-project-members/create-project-members.page.scss b/src/app/dashboard/pages/create-project-members/create-project-members.page.scss deleted file mode 100644 index d5d5b6e62..000000000 --- a/src/app/dashboard/pages/create-project-members/create-project-members.page.scss +++ /dev/null @@ -1,25 +0,0 @@ -:host { - display: block; - padding: 2rem; -} - -form { - padding-left: 10px; -} - -.button { - display: flex; - width: 100%; - justify-content: flex-end; - margin-top: -60px; - margin-bottom: 20px; -} - -.app-member-list { - padding-left: 10px; -} - -.app-button { - display: flex; - margin-top: 2rem; -} diff --git a/src/app/dashboard/pages/create-project-members/create-project-members.page.spec.ts b/src/app/dashboard/pages/create-project-members/create-project-members.page.spec.ts deleted file mode 100644 index bf0919a2a..000000000 --- a/src/app/dashboard/pages/create-project-members/create-project-members.page.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* tslint:disable:no-unused-variable */ -import { HttpClientModule } from '@angular/common/http'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { NgControl, ReactiveFormsModule } from '@angular/forms'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { RouterTestingModule } from '@angular/router/testing'; -import { MainModule } from '@main/_main.module'; - -import { CreateProjectMembersPage } from './create-project-members.page'; - -describe(CreateProjectMembersPage.name, () => { - let component: CreateProjectMembersPage; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [ - MainModule, - BrowserAnimationsModule, - ReactiveFormsModule, - HttpClientModule, - RouterTestingModule, - ], - declarations: [CreateProjectMembersPage], - providers: [NgControl], - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(CreateProjectMembersPage); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/dashboard/pages/create-project-members/create-project-members.page.ts b/src/app/dashboard/pages/create-project-members/create-project-members.page.ts deleted file mode 100644 index d45ed4546..000000000 --- a/src/app/dashboard/pages/create-project-members/create-project-members.page.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Component } from '@angular/core'; -import { FormControl, FormGroup } from '@ngneat/reactive-forms'; -import { ActivatedRoute, Router } from '@angular/router'; -import { AddMemberDialog } from '@dashboard/dialogs/add-member/add-member.dialog'; -import { Workspace } from '@dashboard/interfaces/workspace.interface'; -import { MemberService } from '@dashboard/services/member/member.service'; -import { faPlus } from '@fortawesome/free-solid-svg-icons'; -import { DialogService } from '@main/services/dialog/dialog.service'; -import { maxLengthValidator } from '@main/validators/max-length.validator'; -import { Observable, Subscription } from 'rxjs'; -import { ProjectService } from '../../services/project/project.service'; -import { WorkspaceService } from '../../services/workspace/workspace.service'; -import { requiredValidator } from '@main/validators/required.validator'; - -@Component({ - selector: 'app-create-project-members', - templateUrl: './create-project-members.page.html', - styleUrls: ['./create-project-members.page.scss'], -}) -export class CreateProjectMembersPage { - faPlus = faPlus; - - /** - * Form group for the project creation. - */ - public form = new FormGroup({ - name: new FormControl('', [requiredValidator(), maxLengthValidator(50)], []), - workspaceId: new FormControl(0, [requiredValidator()]), - }); - - /** - * Subscription to the workspace creation. - */ - public createSubscription?: Subscription; - - public workspace$!: Observable; - - private workspaceId!: number; - - private memberList?: string[]; - - /** - * Default constructor. Injects the Workspace and Router service. - * @param workspaceService Workspace service - * @param router Router service - */ - constructor( - private workspaceService: WorkspaceService, - private projectService: ProjectService, - private router: Router, - private activatedRoute: ActivatedRoute, - private dialogService: DialogService, - private memberService: MemberService, - ) { - const { workspaceId } = this.activatedRoute.snapshot.params; - this.workspaceId = workspaceId; - this.workspace$ = this.workspaceService.get(workspaceId); - this.form.get('workspaceId').setValue(workspaceId); - } - - /** - * Creates a new workspace. Passes the form data to the workspace service. Then navigates to the workspace list if form was valid. - * Otherwise, displays an error message. - */ - - openAddMembersDialog() { - this.dialogService - .open(AddMemberDialog, {}) - .afterClosed() - .subscribe((result) => { - this.memberList = result; - }); - } - - public submitCreate(): void { - if (!this.createSubscription?.closed && this.createSubscription) return; - this.form.markAllAsTouched(); - this.form.updateValueAndValidity(); - if (this.form.invalid) return; - - this.createSubscription = this.projectService.create(this.form.value).subscribe((response) => { - if (this.memberList) { - this.memberService.add(this.memberList, [response.id]).subscribe(() => { - this.router.navigate(['/', 'workspaces', this.workspaceId]).then(() => { - window.location.reload(); - }); - }); - } else { - this.router.navigate(['/', 'workspaces', this.workspaceId]).then(() => { - window.location.reload(); - }); - } - }); - } -} diff --git a/src/app/dashboard/pages/create-project/create-project.page.ts b/src/app/dashboard/pages/create-project/create-project.page.ts index 295191d9e..51b9dd52e 100644 --- a/src/app/dashboard/pages/create-project/create-project.page.ts +++ b/src/app/dashboard/pages/create-project/create-project.page.ts @@ -101,7 +101,9 @@ export class CreateProjectPage { stopLoader(this.loader), ) as Observable ).subscribe((project) => { - this.router.navigate(['/', 'projects', project.id]); + this.router.navigate(['/', 'projects', project.id]).then(() => { + location.reload(); + }); }); } diff --git a/src/app/dashboard/pages/create-workspace/create-workspace.page.html b/src/app/dashboard/pages/create-workspace/create-workspace.page.html index ef805675c..509f386fb 100644 --- a/src/app/dashboard/pages/create-workspace/create-workspace.page.html +++ b/src/app/dashboard/pages/create-workspace/create-workspace.page.html @@ -5,7 +5,7 @@

Create new workspace

Save - Cancel + Cancel
diff --git a/src/app/dashboard/pages/create-workspace/create-workspace.page.ts b/src/app/dashboard/pages/create-workspace/create-workspace.page.ts index 122630a62..73e6de760 100644 --- a/src/app/dashboard/pages/create-workspace/create-workspace.page.ts +++ b/src/app/dashboard/pages/create-workspace/create-workspace.page.ts @@ -1,11 +1,12 @@ import { Component } from '@angular/core'; import { FormControl, FormGroup } from '@ngneat/reactive-forms'; import { Router } from '@angular/router'; -import { maxLengthValidator } from '@main/validators/max-length.validator'; import { Subscription } from 'rxjs'; import { Page } from '@main/decorators/page/page.decorator'; import { requiredValidator } from 'src/app/_main/validators/required.validator'; import { WorkspaceService } from '../../services/workspace/workspace.service'; +import { lengthValidator } from '@main/validators/length.validator'; +import { notEmptyValidator } from '@main/validators/not-empty.validator'; /** * Create workspace page component. @@ -21,7 +22,7 @@ export class CreateWorkspacePage { * Form group for the workspace creation. */ public form = new FormGroup({ - name: new FormControl('', [requiredValidator(), maxLengthValidator(50)], []), + name: new FormControl('', [requiredValidator(), lengthValidator(1, 50), notEmptyValidator()]), }); /** diff --git a/src/app/dashboard/pages/dashboard/dashboard.page.html b/src/app/dashboard/pages/dashboard/dashboard.page.html new file mode 100644 index 000000000..7c88d81a4 --- /dev/null +++ b/src/app/dashboard/pages/dashboard/dashboard.page.html @@ -0,0 +1,2 @@ + + diff --git a/src/app/dashboard/pages/dashboard/dashboard.page.scss b/src/app/dashboard/pages/dashboard/dashboard.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/dashboard/pages/dashboard/dashboard.page.ts b/src/app/dashboard/pages/dashboard/dashboard.page.ts new file mode 100644 index 000000000..deefb631b --- /dev/null +++ b/src/app/dashboard/pages/dashboard/dashboard.page.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'dashboard-page', + templateUrl: './dashboard.page.html', + styleUrls: ['./dashboard.page.scss'], +}) +export class DashboardPage {} diff --git a/src/app/dashboard/pages/edit-workspace/edit-workspace.page.html b/src/app/dashboard/pages/edit-workspace/edit-workspace.page.html index b7c376004..fdd11bb96 100644 --- a/src/app/dashboard/pages/edit-workspace/edit-workspace.page.html +++ b/src/app/dashboard/pages/edit-workspace/edit-workspace.page.html @@ -5,6 +5,6 @@

Edit workspace: {{ (workspace$ | async)?
Save - Cancel + Cancel
diff --git a/src/app/dashboard/pages/edit-workspace/edit-workspace.page.ts b/src/app/dashboard/pages/edit-workspace/edit-workspace.page.ts index 1418be79e..a22f09a27 100644 --- a/src/app/dashboard/pages/edit-workspace/edit-workspace.page.ts +++ b/src/app/dashboard/pages/edit-workspace/edit-workspace.page.ts @@ -2,10 +2,11 @@ import { Component } from '@angular/core'; import { FormControl, FormGroup } from '@ngneat/reactive-forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Workspace } from '@dashboard/interfaces/workspace.interface'; -import { maxLengthValidator } from '@main/validators/max-length.validator'; import { Observable, Subscription } from 'rxjs'; import { requiredValidator } from 'src/app/_main/validators/required.validator'; import { WorkspaceService } from '../../services/workspace/workspace.service'; +import { lengthValidator } from '@main/validators/length.validator'; +import { notEmptyValidator } from '@main/validators/not-empty.validator'; /** * Edit workspace page component. @@ -20,7 +21,7 @@ export class EditWorkspacePage { * Form group for the workspace editing. */ public form = new FormGroup({ - name: new FormControl('', [requiredValidator(), maxLengthValidator(50)], []), + name: new FormControl('', [requiredValidator(), lengthValidator(1, 50), notEmptyValidator()]), id: new FormControl(-1), }); diff --git a/src/app/dashboard/pages/projects-list/projects-list.page.html b/src/app/dashboard/pages/projects-list/projects-list.page.html index d31233633..ecfe6b135 100644 --- a/src/app/dashboard/pages/projects-list/projects-list.page.html +++ b/src/app/dashboard/pages/projects-list/projects-list.page.html @@ -1,48 +1,69 @@ - -
-

{{ (workspace$ | async)?.name }}

- New project -
- - - - - - - - - + +
+ +
+
- - - - + + +
+
+

No projects found

+

It looks like you don't have any projects in this workspace. To create a new project click on the button below.

+ Create project +
+
- - -
- - + + +
+

{{ (workspace$ | async)?.name }}

+ New project +
- - -
- - + +
Name {{ row.name }} Statistics - Last update - - - - - - -
+ + + + + + - - -
Name {{ row.name }}
+ + + Statistics + - + + + + + Last update + - + + + + + + + + + + + + + + + + + + + diff --git a/src/app/dashboard/pages/projects-list/projects-list.page.scss b/src/app/dashboard/pages/projects-list/projects-list.page.scss index e87d7493d..c7e69ee2a 100644 --- a/src/app/dashboard/pages/projects-list/projects-list.page.scss +++ b/src/app/dashboard/pages/projects-list/projects-list.page.scss @@ -1,4 +1,33 @@ :host { display: block; + height: 100%; padding: 2rem; } + +.loader, +.empty { + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; +} + +.content { + display: flex; + max-width: 600px; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +h2 { + color: var(--color-text); + font-size: 1.5rem; +} + +p { + color: var(--color-text); + opacity: 0.75; + text-align: center; +} diff --git a/src/app/dashboard/pages/projects-list/projects-list.page.ts b/src/app/dashboard/pages/projects-list/projects-list.page.ts index 90a08ae98..fc2fe1c90 100644 --- a/src/app/dashboard/pages/projects-list/projects-list.page.ts +++ b/src/app/dashboard/pages/projects-list/projects-list.page.ts @@ -1,12 +1,14 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { WorkspaceService } from '../../services/workspace/workspace.service'; -import { map, Observable } from 'rxjs'; +import { map, Observable, EMPTY } from 'rxjs'; import { Project } from '../../interfaces/project.interface'; import { Workspace } from '../../interfaces/workspace.interface'; import { faPlus } from '@fortawesome/free-solid-svg-icons'; import { DialogService } from '@main/services/dialog/dialog.service'; import { ProjectService } from '../../services/project/project.service'; +import { Loader } from '../../../_main/classes/loader/loader.class'; +import { withLoader, startLoader, stopLoader } from '../../../_main/operators/loader.operator'; /** * Projects list page component @@ -16,29 +18,39 @@ import { ProjectService } from '../../services/project/project.service'; templateUrl: './projects-list.page.html', styleUrls: ['./projects-list.page.scss'], }) -export class ProjectsListPage { +export class ProjectsListPage implements OnInit { /** Workspace object */ - workspace$: Observable; + workspace$: Observable = EMPTY; /** List of projects */ - projects$: Observable; + projects$: Observable = EMPTY; /** @ignore */ faPlus = faPlus; + loader = new Loader(); + + workspaceId!: number; + constructor( private activatedRoute: ActivatedRoute, private workspaceService: WorkspaceService, private projectService: ProjectService, private dialogService: DialogService, private router: Router, - ) { - const { workspaceId } = this.activatedRoute.snapshot.params; + ) {} - this.workspace$ = this.workspaceService.get(workspaceId); - this.projects$ = this.workspace$.pipe( - map((workspace) => workspace.projectsWithPrivileges.map((project) => project.project)), - ); + ngOnInit() { + this.activatedRoute.params.subscribe(({ workspaceId }) => { + this.workspaceId = workspaceId; + + this.loader.markAsPending(); + this.workspace$ = this.workspaceService.get(workspaceId); + this.projects$ = this.workspace$.pipe( + map((workspace) => workspace.projectsWithPrivileges.map((project) => project.project)), + withLoader(this.loader), + ); + }); } /** Navigate to project edit page */ diff --git a/src/app/dashboard/pages/workspaces-list/workspaces-list.page.html b/src/app/dashboard/pages/workspaces-list/workspaces-list.page.html index 96ae51edc..af834fe46 100644 --- a/src/app/dashboard/pages/workspaces-list/workspaces-list.page.html +++ b/src/app/dashboard/pages/workspaces-list/workspaces-list.page.html @@ -1,48 +1,66 @@ - -
-

Workspaces

- New workspace -
- - - - - - - - - - - - - - - - - - - - - - - - - - + +
Name {{ row.name }} Statistics - Last update - - - - -
+ diff --git a/src/app/dashboard/pages/workspaces-list/workspaces-list.page.scss b/src/app/dashboard/pages/workspaces-list/workspaces-list.page.scss index e87d7493d..c7e69ee2a 100644 --- a/src/app/dashboard/pages/workspaces-list/workspaces-list.page.scss +++ b/src/app/dashboard/pages/workspaces-list/workspaces-list.page.scss @@ -1,4 +1,33 @@ :host { display: block; + height: 100%; padding: 2rem; } + +.loader, +.empty { + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; +} + +.content { + display: flex; + max-width: 600px; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +h2 { + color: var(--color-text); + font-size: 1.5rem; +} + +p { + color: var(--color-text); + opacity: 0.75; + text-align: center; +} diff --git a/src/app/dashboard/pages/workspaces-list/workspaces-list.page.ts b/src/app/dashboard/pages/workspaces-list/workspaces-list.page.ts index 05765839d..95c94c72c 100644 --- a/src/app/dashboard/pages/workspaces-list/workspaces-list.page.ts +++ b/src/app/dashboard/pages/workspaces-list/workspaces-list.page.ts @@ -1,12 +1,14 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, Pipe } from '@angular/core'; import { Router } from '@angular/router'; import { faPlus } from '@fortawesome/free-solid-svg-icons'; -import { Observable } from 'rxjs'; +import { Observable, of, switchMap } from 'rxjs'; import { Page } from '@main/decorators/page/page.decorator'; import { DialogService } from '@main/services/dialog/dialog.service'; import { Workspace } from '../../interfaces/workspace.interface'; import { ProjectService } from '../../services/project/project.service'; import { WorkspaceService } from '../../services/workspace/workspace.service'; +import { Loader } from '../../../_main/classes/loader/loader.class'; +import { withLoader, startLoader, stopLoader } from '../../../_main/operators/loader.operator'; /** * Workspaces list page component. @@ -26,14 +28,11 @@ export class WorkspacesListPage implements OnInit { */ constructor( private workspaceService: WorkspaceService, - private projectService: ProjectService, private dialogService: DialogService, private router: Router, ) {} - /** - * Plus icon to display on the add button - */ + /** @ignore */ public faPlus = faPlus; /** @@ -41,9 +40,8 @@ export class WorkspacesListPage implements OnInit { */ public workspaces$?: Observable; - /** - * Lifecycle hook to load workspaces at the start of the page. - */ + public loader = new Loader(); + ngOnInit() { this.loadWorkspaces(); } @@ -52,7 +50,12 @@ export class WorkspacesListPage implements OnInit { * Loads the workspaces list from the workspace service. */ loadWorkspaces() { - this.workspaces$ = this.workspaceService.list(); + this.loader.markAsPending(); + this.workspaces$ = of(null).pipe( + startLoader(this.loader), + switchMap(() => this.workspaceService.list()), + stopLoader(this.loader), + ); } /** diff --git a/src/app/messages/messages.module.ts b/src/app/messages/messages.module.ts index 7794334d0..51e399ce7 100644 --- a/src/app/messages/messages.module.ts +++ b/src/app/messages/messages.module.ts @@ -7,6 +7,7 @@ import { MessengerPage } from './pages/messenger/messenger.page'; import { MainModule } from '@main/_main.module'; import { MessageComponent } from './components/message/message.component'; import { MessageGroupComponent } from './components/message-group/message-group.component'; +import { MessagesNoIntegration } from './pages/messages-no-integration/messages-no-integration.page'; /** Communication module with all messaging tools and pages */ @NgModule({ @@ -17,6 +18,7 @@ import { MessageGroupComponent } from './components/message-group/message-group. MessengerPage, MessageComponent, MessageGroupComponent, + MessagesNoIntegration, ], exports: [MessageComponent], }) diff --git a/src/app/messages/messages.routing.ts b/src/app/messages/messages.routing.ts index 1ac47eb8c..313ec8e51 100644 --- a/src/app/messages/messages.routing.ts +++ b/src/app/messages/messages.routing.ts @@ -3,6 +3,7 @@ import { Routes, RouterModule } from '@angular/router'; import { ConversationPage } from './pages/conversation/conversation.page'; import { MessengerSummaryPage } from './pages/messenger-summary/messenger-summary.page'; import { MessengerPage } from './pages/messenger/messenger.page'; +import { MessagesNoIntegration } from './pages/messages-no-integration/messages-no-integration.page'; /** * Messages routes list @@ -17,6 +18,10 @@ const routes: Routes = [ pathMatch: 'full', component: MessengerSummaryPage, }, + { + path: 'no-integration', + component: MessagesNoIntegration, + }, { path: ':integrationId/:channelId', component: ConversationPage, diff --git a/src/app/messages/pages/conversation/conversation.page.ts b/src/app/messages/pages/conversation/conversation.page.ts index c76354111..21f64c672 100644 --- a/src/app/messages/pages/conversation/conversation.page.ts +++ b/src/app/messages/pages/conversation/conversation.page.ts @@ -7,6 +7,9 @@ import { EMPTY, map, Observable, shareReplay, BehaviorSubject, tap } from 'rxjs' import { SlackIntegrationService } from '@messages/services/slack-integration.service'; import { SlackChannel } from '../../interfaces/slack.interface'; import { memoize } from 'lodash-es'; +import { notEmptyValidator } from '@main/validators/not-empty.validator'; +import { requiredValidator } from '@main/validators/required.validator'; +import { validateForm } from '@main/classes/form.class'; /** * Conversation page component @@ -22,7 +25,7 @@ export class ConversationPage implements OnInit { /** Message form group */ public form = new FormGroup({ - message: new FormControl(''), + message: new FormControl('', [requiredValidator(), notEmptyValidator()]), }); /** Id of the integration */ @@ -85,6 +88,8 @@ export class ConversationPage implements OnInit { * Send message */ sendMessage() { + if (!validateForm(this.form)) return; + this.channel$.subscribe((channel) => { this.slackIntegrationService.sendMessage( this.form.value.message, diff --git a/src/app/messages/pages/messages-no-integration/messages-no-integration.page.html b/src/app/messages/pages/messages-no-integration/messages-no-integration.page.html new file mode 100644 index 000000000..e13db85d7 --- /dev/null +++ b/src/app/messages/pages/messages-no-integration/messages-no-integration.page.html @@ -0,0 +1,5 @@ +
+

No Slack integration found

+

Messages module require Slack integration to work. To connect your account with Slack you need to add it in settings page.

+ Go to Settings +
diff --git a/src/app/messages/pages/messages-no-integration/messages-no-integration.page.scss b/src/app/messages/pages/messages-no-integration/messages-no-integration.page.scss new file mode 100644 index 000000000..573b2eff8 --- /dev/null +++ b/src/app/messages/pages/messages-no-integration/messages-no-integration.page.scss @@ -0,0 +1,26 @@ +:host { + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; +} + +.content { + display: flex; + max-width: 600px; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +h2 { + color: var(--color-text); + font-size: 1.5rem; +} + +p { + color: var(--color-text); + opacity: 0.75; + text-align: center; +} diff --git a/src/app/messages/pages/messages-no-integration/messages-no-integration.page.ts b/src/app/messages/pages/messages-no-integration/messages-no-integration.page.ts new file mode 100644 index 000000000..0da539bef --- /dev/null +++ b/src/app/messages/pages/messages-no-integration/messages-no-integration.page.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'messages-no-integration', + templateUrl: './messages-no-integration.page.html', + styleUrls: ['./messages-no-integration.page.scss'], +}) +export class MessagesNoIntegration {} diff --git a/src/app/releases/dialog/release/release.dialog.ts b/src/app/releases/dialog/release/release.dialog.ts index 27d14624c..42f5f54d7 100644 --- a/src/app/releases/dialog/release/release.dialog.ts +++ b/src/app/releases/dialog/release/release.dialog.ts @@ -8,6 +8,8 @@ import { requiredValidator } from '../../../_main/validators/required.validator' import { unixTimestamp } from '../../../_main/interfaces/date.interface'; import { ProjectService } from '@dashboard/services/project/project.service'; import { Release } from '../../interfaces/release.interface'; +import { lengthValidator } from '@main/validators/length.validator'; +import { notEmptyValidator } from '@main/validators/not-empty.validator'; /** Interface to represent release dialog variant */ export enum ReleaseDialogVariant { @@ -44,8 +46,12 @@ export class ReleaseDialog implements OnInit { public form = new FormGroup({ id: new FormControl(null), projectId: new FormControl(null, [requiredValidator()]), - name: new FormControl('', [requiredValidator()]), - description: new FormControl(''), + name: new FormControl('', [ + requiredValidator(), + lengthValidator(1, 50), + notEmptyValidator(), + ]), + description: new FormControl('', [notEmptyValidator()]), deadline: new FormControl(null, [requiredValidator()]), }); diff --git a/src/app/settings/dialog/change-password/change-password.dialog.html b/src/app/settings/dialog/change-password/change-password.dialog.html new file mode 100644 index 000000000..0d44e0f3b --- /dev/null +++ b/src/app/settings/dialog/change-password/change-password.dialog.html @@ -0,0 +1,26 @@ +

Change password

+
+
+ +
+ {{ error }} +
+ + + + + + + + + + + + + +
+
+
+ Cancel + Save +
diff --git a/src/app/settings/dialog/change-password/change-password.dialog.scss b/src/app/settings/dialog/change-password/change-password.dialog.scss new file mode 100644 index 000000000..d26a3e884 --- /dev/null +++ b/src/app/settings/dialog/change-password/change-password.dialog.scss @@ -0,0 +1,9 @@ +form { + display: flex; + flex-direction: column; + padding-top: 1rem; + + &>*:not(:last-child):not(app-checkbox) { + margin-bottom: 1.5rem; + } +} diff --git a/src/app/settings/dialog/change-password/change-password.dialog.ts b/src/app/settings/dialog/change-password/change-password.dialog.ts new file mode 100644 index 000000000..e448cfcb6 --- /dev/null +++ b/src/app/settings/dialog/change-password/change-password.dialog.ts @@ -0,0 +1,93 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { requiredValidator } from '@main/validators/required.validator'; +import { FormControl, FormGroup } from '@ngneat/reactive-forms'; +import { validateForm } from '@main/classes/form.class'; +import { AuthService } from '@auth/services/auth/auth.service'; +import { SnackbarService } from '@main/services/snackbar/snackbar.service'; +import { catchError, throwError } from 'rxjs'; +import { withLoader } from '@main/operators/loader.operator'; +import { Loader } from '@main/classes/loader/loader.class'; +import { passwordValidator } from '@main/validators/password.validator'; +import { sameAsValidator } from '@main/validators/same-as.validator'; + +/** + * Change password dialog component + */ +@Component({ + selector: 'change-password-dialog', + templateUrl: './change-password.dialog.html', + styleUrls: ['./change-password.dialog.scss'], +}) +export class ChangePasswordDialog implements OnInit { + /** + * Login error + */ + public error?: string; + + /** + * Loader + */ + public loader = new Loader(); + + /** Change password dialog form */ + public form = new FormGroup({ + oldPassword: new FormControl('', [requiredValidator(), passwordValidator()]), + newPassword: new FormControl('', [requiredValidator(), passwordValidator()]), + rep_newPassword: new FormControl('', [ + requiredValidator(), + passwordValidator(), + sameAsValidator('newPassword', $localize`Given passwords are not the same`), + ]), + }); + + constructor( + private dialogRef: MatDialogRef, + private authService: AuthService, + private snackbarService: SnackbarService, + ) {} + + ngOnInit() {} + + /** + * Cancel dialog + */ + cancel() { + this.dialogRef.close(); + } + + /** + * Closes dialog + */ + save() { + if (validateForm(this.form)) { + this.error = undefined; + let data = this.form.value; + this.authService + .changePassword(data.oldPassword, data.newPassword) + .pipe( + withLoader(this.loader), + catchError((error) => { + this.handleError(error); + return throwError(() => new Error(error)); + }), + ) + .subscribe(() => { + this.dialogRef.close(); + this.snackbarService.show($localize`Password changed successfully!`); + }); + } + } + + /** + * Handle chaneg password error + * @param error Error + */ + handleError(error: any) { + switch (error.status) { + case 404: + this.error = $localize`Current password is incorrect.`; + break; + } + } +} diff --git a/src/app/settings/pages/settings-account/settings-account.page.html b/src/app/settings/pages/settings-account/settings-account.page.html index 3818fa12b..341accdfd 100644 --- a/src/app/settings/pages/settings-account/settings-account.page.html +++ b/src/app/settings/pages/settings-account/settings-account.page.html @@ -32,7 +32,7 @@

Account

It's highly recommended to change the password every 30 days. It will increase your account security.
- Change password + Change password
diff --git a/src/app/settings/pages/settings-account/settings-account.page.ts b/src/app/settings/pages/settings-account/settings-account.page.ts index 17dd3a956..57cd80c22 100644 --- a/src/app/settings/pages/settings-account/settings-account.page.ts +++ b/src/app/settings/pages/settings-account/settings-account.page.ts @@ -4,6 +4,8 @@ import { AuthService } from '@auth/services/auth/auth.service'; import { UserService } from '@auth/services/user/user.service'; import { requiredValidator } from '@main/validators/required.validator'; import { SnackbarService } from '@main/services/snackbar/snackbar.service'; +import { Router } from '@angular/router'; +import { notEmptyValidator } from '@main/validators/not-empty.validator'; /** * Settings account page component @@ -18,13 +20,14 @@ export class SettingsAccountPage implements OnInit { private userService: UserService, private authService: AuthService, private snackbarService: SnackbarService, + private router: Router, ) {} /** Form to edit account data */ public form = new FormGroup({ email: new FormControl('', requiredValidator()), - name: new FormControl('', requiredValidator()), - surname: new FormControl('', requiredValidator()), + name: new FormControl('', [requiredValidator(), notEmptyValidator()]), + surname: new FormControl('', [requiredValidator(), notEmptyValidator()]), username: new FormControl('', requiredValidator()), }); @@ -48,15 +51,11 @@ export class SettingsAccountPage implements OnInit { } /** - * Reset password + * Change password * @TODO Add confirmation dialog */ - resetPassword() { - const email = this.form.get('email').value; - - this.authService.resetPassword({ email }).subscribe(() => { - this.authService.logout().subscribe(); - }); + changePassword() { + this.authService.openChangePasswordDialog().subscribe(); } /** @@ -64,8 +63,8 @@ export class SettingsAccountPage implements OnInit { * @TODO Add confirmation dialog */ deleteAccountMailCheck() { - this.authService.deleteAccount().subscribe(() => { - this.authService.logout().subscribe(); + this.authService.deleteAccountWithConfirmation().subscribe(() => { + this.router.navigate(['/']); }); } } diff --git a/src/app/settings/pages/settings-localization/settings-localization.page.ts b/src/app/settings/pages/settings-localization/settings-localization.page.ts index ae887d129..4d93ec84b 100644 --- a/src/app/settings/pages/settings-localization/settings-localization.page.ts +++ b/src/app/settings/pages/settings-localization/settings-localization.page.ts @@ -18,9 +18,9 @@ export class SettingsLocalizationPage implements OnInit { /** Form to change localization settings */ public form = new FormGroup({ language: new FormControl('', requiredValidator()), - dateFormat: new FormControl(''), - timeFormat: new FormControl(''), - firstDayOfWeek: new FormControl(0), + dateFormat: new FormControl('', requiredValidator()), + timeFormat: new FormControl('', requiredValidator()), + firstDayOfWeek: new FormControl(0, requiredValidator()), }); /** diff --git a/src/app/settings/settings.module.ts b/src/app/settings/settings.module.ts index a493bfeea..cae922db2 100644 --- a/src/app/settings/settings.module.ts +++ b/src/app/settings/settings.module.ts @@ -5,6 +5,7 @@ import { MainModule } from '@main/_main.module'; import { SettingsPage } from 'src/app/settings/pages/settings/settings.page'; import { IntegrationEntryComponent } from './components/integration-entry/integration-entry.component'; import { ListGroupComponent } from './components/list-group/list-group.component'; +import { ChangePasswordDialog } from './dialog/change-password/change-password.dialog'; import { SettingsAccountPage } from './pages/settings-account/settings-account.page'; import { SettingsIntegrationsPage } from './pages/settings-integrations/settings-integrations.page'; import { SettingsLocalizationPage } from './pages/settings-localization/settings-localization.page'; @@ -21,6 +22,7 @@ import { SettingsRoutingModule } from './settings.routing'; SettingsSessionsPage, ListGroupComponent, IntegrationEntryComponent, + ChangePasswordDialog, ], }) export class SettingsModule {} diff --git a/src/app/tasks/components/task-comments/task-comments.component.ts b/src/app/tasks/components/task-comments/task-comments.component.ts index 2acb16cba..08d9a551b 100644 --- a/src/app/tasks/components/task-comments/task-comments.component.ts +++ b/src/app/tasks/components/task-comments/task-comments.component.ts @@ -4,6 +4,8 @@ import { FormGroup, FormControl } from '@ngneat/reactive-forms'; import { requiredValidator } from '../../../_main/validators/required.validator'; import { EMPTY, Observable } from 'rxjs'; import { Comment } from '../../interfaces/comment.interface'; +import { lengthValidator } from '@main/validators/length.validator'; +import { notEmptyValidator } from '@main/validators/not-empty.validator'; /** * Component to display comments for task @@ -25,7 +27,11 @@ export class TaskCommentsComponent implements OnInit { /** Form to create new comment */ public form = new FormGroup({ - content: new FormControl('', [requiredValidator()]), + content: new FormControl('', [ + requiredValidator(), + lengthValidator(1, 1000), + notEmptyValidator(), + ]), }); constructor(private commentService: CommentService) {} diff --git a/src/app/tasks/components/task-list/task-list.component.html b/src/app/tasks/components/task-list/task-list.component.html index 755940d0b..82a104aaf 100644 --- a/src/app/tasks/components/task-list/task-list.component.html +++ b/src/app/tasks/components/task-list/task-list.component.html @@ -2,12 +2,12 @@
-
Title
-
Status
-
Assignee
-
Time tracking
-
Deadline
-
Story points
+
Title
+
Status
+
Assignee
+
Time tracking
+
Deadline
+
Story points
@@ -16,7 +16,7 @@
+ [members]="members" [statusList]="statusList" [columns]="columns">