diff --git a/src/app/core/services/private-catalogs.service.ts b/src/app/core/services/private-catalogs.service.ts index 2b38f8f187c2a68db7e67a358920ee225216fff2..3c4e88c6fa497e0832ef3d3078d43d1254034e58 100644 --- a/src/app/core/services/private-catalogs.service.ts +++ b/src/app/core/services/private-catalogs.service.ts @@ -4,12 +4,14 @@ import { Observable, catchError, map, tap } from 'rxjs'; import { HttpSharedService } from '../http-shared/http-shared.service'; import { HttpClient, HttpEvent, HttpRequest } from '@angular/common/http'; import { + AuthorPublisherModel, CommentModel, PublicSolution, ThreadModel, UserDetails, } from 'src/app/shared/models'; import { CreateRatingRequestPayload } from 'src/app/shared/models/request-payloads'; +import { MessageStatusModel } from 'src/app/shared/models/message-status.model'; @Injectable({ providedIn: 'root', @@ -122,7 +124,7 @@ export class PrivateCatalogsService { ); } - uploadFileToUrl( + uploadExistingModelLicenseProfile( loginUserID: string, solutionId: string, revisionId: string, @@ -140,8 +142,6 @@ export class PrivateCatalogsService { revisionId + '/' + versionId; - const uploadUrl = - 'https://dev02.ki-lab.nrw/api/license/upload/9b36ce33-ec9c-48e3-964f-5fd7e75ede6c/620ec698-4027-414a-8b9f-8d6503cbf35b/86e0f4fd-694a-4ba2-ba9f-bb3db1dc850f/1.0.0'; const formData = new FormData(); formData.append('file', file); @@ -154,6 +154,24 @@ export class PrivateCatalogsService { return this.http.request(req); } + uploadNewModelLicenseProfile(userId: string, file: File) { + const url = + apiConfig.apiBackendURL + + '/api/model/upload/' + + userId + + '/?licUploadFlag=' + + true; + const formData = new FormData(); + formData.append('file', file); + + const req = new HttpRequest('POST', url, formData, { + reportProgress: true, + responseType: 'json', + }); + + return this.http.request(req); + } + createUpdateLicenseProfile( loginUserID: string, solutionId: string, @@ -377,4 +395,136 @@ export class PrivateCatalogsService { }), ); } + + getAuthors( + solutionId: string, + revisionId: string, + ): Observable<AuthorPublisherModel[]> { + const url = `${apiConfig.apiBackendURL}/api/solution/${solutionId}/revision/${revisionId}/authors`; + return this._httpSharedService.get(url, undefined).pipe( + map((res) => res.response_body), + catchError((error) => { + throw error; + }), + ); + } + + addAuthor( + solutionId: string, + revisionId: string, + author: AuthorPublisherModel, + ): Observable<AuthorPublisherModel[]> { + const url = `${apiConfig.apiBackendURL}/api/solution/${solutionId}/revision/${revisionId}/authors`; + const obj = { + request_body: author, + }; + + return this._httpSharedService.put(url, undefined, obj).pipe( + map((res) => res.response_body), + catchError((error) => { + throw error; + }), + ); + } + + removeAuthor( + solutionId: string, + revisionId: string, + author: AuthorPublisherModel, + ): Observable<AuthorPublisherModel[]> { + const url = `${apiConfig.apiBackendURL}/api/solution/${solutionId}/revision/${revisionId}/removeAuthor`; + const obj = { + request_body: author, + }; + + return this._httpSharedService.put(url, undefined, obj).pipe( + map((res) => res.response_body), + catchError((error) => { + throw error; + }), + ); + } + + getPublisher(solutionId: string, revisionId: string): Observable<string> { + const url = `${apiConfig.apiBackendURL}/api/solution/${solutionId}/revision/${revisionId}/publisher`; + return this._httpSharedService.get(url, undefined).pipe( + map((res) => res.response_body), + catchError((error) => { + throw error; + }), + ); + } + + updatePublisher( + solutionId: string, + revisionId: string, + publisherName: string, + ) { + const url = `${apiConfig.apiBackendURL}/api/solution/${solutionId}/revision/${revisionId}/publisher`; + const obj = publisherName; + + return this._httpSharedService.put(url, undefined, obj).pipe( + catchError((error) => { + throw error; + }), + ); + } + + addCatalog( + userId: string, + solutionName: string, + dockerUrl: string, + ): Observable<string> { + const urlCreateFavorite = + apiConfig.apiBackendURL + apiConfig.urlAddToCatalog + '/' + userId; + + const addToReqObj = { + request_body: { + name: solutionName, + dockerfileURI: dockerUrl, + }, + }; + + return this._httpSharedService + .post(urlCreateFavorite, undefined, addToReqObj) + .pipe( + map((res) => res.response_detail), + catchError((error) => { + throw error; + }), + ); + } + uploadProtoBufFile(file: File) { + const url = + apiConfig.apiBackendURL + '/api/proto/upload/' + '?protoUploadFlag=true'; + + const formData = new FormData(); + formData.append('file', file); + + const req = new HttpRequest('POST', url, formData, { + reportProgress: true, + responseType: 'json', + }); + + return this.http.request(req); + } + + getMessagingStatus( + userId: string, + trackId: string, + ): Observable<MessageStatusModel[]> { + const url = + apiConfig.apiBackendURL + + apiConfig.urlMessagingStatus + + '/' + + userId + + '/' + + trackId; + return this._httpSharedService.post(url, undefined).pipe( + map((res) => res.response_body), + catchError((error) => { + throw error; + }), + ); + } } diff --git a/src/app/features/model-details/model-details.component.html b/src/app/features/model-details/model-details.component.html index 9b5e3307a0fd99d8317cd33ecad26143cb8f686a..4b6be61e308c35ae80e4f057cdd114ea6932a80a 100644 --- a/src/app/features/model-details/model-details.component.html +++ b/src/app/features/model-details/model-details.component.html @@ -41,39 +41,11 @@ </mat-menu> </div> </div> - <div class="version-container"> - <button - class="mdl-button" - #menuTrigger2="matMenuTrigger" - [matMenuTriggerFor]="menu2" - (click)="menuTrigger2.openMenu()" - > - <span class="text-ellipsis" - >Version - {{ selectedDefaultRevision?.version }}</span - > - <i class="material-icons">keyboard_arrow_down</i> - </button> - <mat-menu #menu2="matMenu" - ><ul> - <li - *ngFor="let revision of revisionsList" - [ngClass]="{ - selected: revision.version === selectedDefaultRevision.version - }" - (click)="onChangeVersion(revision)" - class="li-border-bottom" - > - <span - [ngClass]="{ - 'md-cat-txtellipsis': revision.version.length > 15 - }" - >{{ revision.version }}</span - > - <span class="custom-tooltip-text">{{ revision.version }}</span> - </li> - </ul></mat-menu - > - </div> + <gp-version-dropdown + [selectedDefaultRevision]="selectedDefaultRevision" + [revisionsList]="revisionsList" + (revisionChange)="onChangeVersion($event)" + ></gp-version-dropdown> </div> <ng-container *ngIf="isLoggedIn$ | async; else notLoggedIn"> <div class="md-head-container2"> diff --git a/src/app/features/model-details/model-details.component.ts b/src/app/features/model-details/model-details.component.ts index a407e6e5609df86941c5b0edd8ac7cfa89bce579..262ad0587c77c9a422516255a4eb5bfba0d94a82 100644 --- a/src/app/features/model-details/model-details.component.ts +++ b/src/app/features/model-details/model-details.component.ts @@ -51,6 +51,7 @@ import { apiConfig } from 'src/app/core/config'; import { PrivateCatalogsService } from 'src/app/core/services/private-catalogs.service'; import { SolutionIdComponent } from 'src/app/shared/components/solution-id/solution-id.component'; import { BreadcrumbNavigationComponent } from 'src/app/shared/components/breadcrumb-navigation/breadcrumb-navigation.component'; +import { VersionDropdownComponent } from 'src/app/shared/components/version-dropdown/version-dropdown.component'; interface SolutionData { solutionId: string; @@ -90,6 +91,7 @@ interface SolutionData { FormsModule, SolutionIdComponent, BreadcrumbNavigationComponent, + VersionDropdownComponent, ], templateUrl: './model-details.component.html', styleUrl: './model-details.component.scss', @@ -103,14 +105,8 @@ export class ModelDetailsComponent implements OnInit { imageToShow: any; isImageLoading = true; selectedDefaultCatalog!: Catalog; - revisionsList!: { - version: string; - revisionId: string; - }[]; - selectedDefaultRevision!: { - version: string; - revisionId: string; - }; + revisionsList!: Revision[]; + selectedDefaultRevision!: Revision; averageRatings!: AverageRatings; allRatings: AllUserRating[] = []; totalRatingsCount: number = 0; @@ -371,7 +367,7 @@ export class ModelDetailsComponent implements OnInit { }); } - onChangeVersion(revision: { version: string; revisionId: string }) { + onChangeVersion(revision: Revision) { this.selectedDefaultRevision = revision; this.setVersionIdInService(); this.setRevisionInService(revision); diff --git a/src/app/shared/components/delete-user-dialog-confirmation-action/delete-user-dialog-confirmation-action.component.html b/src/app/shared/components/delete-user-dialog-confirmation-action/delete-user-dialog-confirmation-action.component.html index 6d7855c75085eda98037d1b057ea38b6bdb73a58..de2757cd2b6a10a83742a2e3e192c7cc43e6216e 100644 --- a/src/app/shared/components/delete-user-dialog-confirmation-action/delete-user-dialog-confirmation-action.component.html +++ b/src/app/shared/components/delete-user-dialog-confirmation-action/delete-user-dialog-confirmation-action.component.html @@ -1,6 +1,6 @@ <mat-toolbar class="dialog-header"> <div style="display: flex; align-items: center; padding: 0; flex: 1 1 auto"> - <h2>Delete Shared Member</h2> + <h2>{{ title }}</h2> </div> <button type="button" @@ -15,8 +15,7 @@ <mat-dialog-content> <div class="star-rating-container"> <p> - Would you like to delete '{{ user.firstName }} {{ user.lastName }}' from - the shared list? + {{ content }} </p> </div></mat-dialog-content > diff --git a/src/app/shared/components/delete-user-dialog-confirmation-action/delete-user-dialog-confirmation-action.component.ts b/src/app/shared/components/delete-user-dialog-confirmation-action/delete-user-dialog-confirmation-action.component.ts index 64dcefb259fcb47873718893ec6a718d72587c21..fbfaa3af718d861ffb25f27fb703fa7dcbcc482a 100644 --- a/src/app/shared/components/delete-user-dialog-confirmation-action/delete-user-dialog-confirmation-action.component.ts +++ b/src/app/shared/components/delete-user-dialog-confirmation-action/delete-user-dialog-confirmation-action.component.ts @@ -27,34 +27,38 @@ import { AlertService } from 'src/app/core/services/alert.service'; styleUrl: './delete-user-dialog-confirmation-action.component.scss', }) export class DeleteUserDialogConfirmationActionComponent implements OnInit { - user!: UserDetails; + title!: string; + content!: string; + alertMessage!: string; constructor( public dialogRef: MatDialogRef<DeleteUserDialogConfirmationActionComponent>, - private privateCatalogsService: PrivateCatalogsService, @Inject(MAT_DIALOG_DATA) public data: any, private alertService: AlertService, ) {} ngOnInit(): void { - this.user = this.data.dataKey.user; + this.title = this.data.dataKey.title; + this.content = this.data.dataKey.content; + this.alertMessage = this.data.dataKey.alertMessage; } confirm() { - this.privateCatalogsService - .deleteShareWithTeam(this.user.userId ?? '', this.data.dataKey.solutionId) - .subscribe((res) => { - const alert: Alert = { - message: '', - type: AlertType.Success, - }; - - if (res.error_code === '100') { - alert.message = 'Deleted successfully. '; - } - - this.alertService.notify(alert, 3000); - this.dialogRef.close(); - }); + this.data.dataKey.action().subscribe({ + next: (res: any) => { + this.alertService.notify( + { message: this.alertMessage, type: AlertType.Success }, + 3000, + ); + this.dialogRef.close(true); + }, + error: (err: any) => { + this.alertService.notify( + { message: 'Operation failed', type: AlertType.Error }, + 3000, + ); + this.dialogRef.close(false); + }, + }); } } diff --git a/src/app/shared/components/manage-publisher-authors-page/manage-publisher-authors-page.component.html b/src/app/shared/components/manage-publisher-authors-page/manage-publisher-authors-page.component.html index 245dc49bd4dc3307fe9bc97d01ef1a883976e467..03d0f1520003d6f184819ad84962e6711e2c066e 100644 --- a/src/app/shared/components/manage-publisher-authors-page/manage-publisher-authors-page.component.html +++ b/src/app/shared/components/manage-publisher-authors-page/manage-publisher-authors-page.component.html @@ -1 +1,196 @@ <div class="workflow-right-header workflow-header">Manage Authors</div> +<div style="display: flex; width: 100%; height: 60%; padding: 20px"> + <div + style=" + display: flex; + flex-direction: column; + padding: 10px; + width: 100%; + gap: 20px; + " + > + <span>PUBLISHER</span> + <mat-divider></mat-divider> + <form + style="display: flex; flex-direction: column; height: 100%" + [formGroup]="publisherForm" + > + <div + style="display: flex; flex-direction: column; flex-grow: 1; gap: 20px" + > + <mat-label>Publisher Name </mat-label> + <div style="display: flex; flex-direction: column"> + <mat-form-field> + <input class="example-full-width" matInput formControlName="name" /> + @if (publisherForm.value.name) { + <button + matSuffix + mat-icon-button + aria-label="Clear" + (click)="publisherForm.setValue({ name: '' })" + > + <mat-icon>close</mat-icon> + </button> + } + @if ( + publisherForm.controls["name"].hasError("required") && + publisherForm.controls["name"].touched + ) { + <mat-error class="modal-error" + >Publisher name is required</mat-error + > + } + + @if ( + publisherForm.controls["name"].errors?.["minlength"] && + publisherForm.controls["name"].touched + ) { + <mat-error class="modal-error" + >Publisher name should contain at least 2 characters.</mat-error + > + } + @if ( + publisherForm.controls["name"].errors?.["pattern"] && + publisherForm.controls["name"].touched + ) { + <mat-error class="modal-error" + >Publisher name must contain only alphanumeric + letters.</mat-error + > + } + @if (publisherForm.get("name")?.hasError("sameAsPublisher")) { + <mat-error class="modal-error" + >Please type in a new publisher name to save changes.</mat-error + > + } + </mat-form-field> + </div> + </div> + + <div style="display: flex; justify-content: space-between"> + <button + style="margin-right: 20px; width: 135px" + mat-raised-button + (click)="onCancelClick()" + > + Discard changes + </button> + <button + style="margin-right: 20px; width: 100px" + color="primary" + mat-raised-button + (click)="OnEditPublisherClick()" + [disabled]="!publisherForm.controls['name'].value" + > + Save + </button> + </div> + </form> + </div> + <mat-divider vertical style="height: auto"></mat-divider> + <div + style=" + display: flex; + flex-direction: column; + padding: 10px; + width: 100%; + height: 100%; + " + > + <div style="display: flex; flex-direction: column; gap: 20px; height: 100%"> + <span>AUTHORS</span> + <mat-divider></mat-divider> + <mat-chip-set #chipGrid> + @for (author of authorsList; track author) { + <mat-chip-row (removed)="onRemoveAuthorClick(author)"> + {{ author.name }} + <button matChipRemove [attr.aria-label]="'remove ' + author.name"> + <mat-icon>cancel</mat-icon> + </button> + </mat-chip-row> + } @empty { + <span>No authors listed.</span> + } + </mat-chip-set> + Please add additional names of one or more author of this model. + <form + style=" + display: flex; + flex-direction: column; + + height: 100%; + " + [formGroup]="authorForm" + #formDirective="ngForm" + > + <div style="display: flex; flex-direction: column; flex-grow: 1"> + <div style="display: flex; flex-direction: column"> + <mat-label class="asterisk--after">Name </mat-label> + <mat-form-field> + <input + class="example-full-width" + matInput + formControlName="name" + /> + @if (authorForm.value.name) { + <button + matSuffix + mat-icon-button + aria-label="Clear" + (click)="authorForm.controls['name'].setValue('')" + > + <mat-icon>close</mat-icon> + </button> + } + @if (authorForm.controls["contact"].hasError("required")) { + <mat-error class="modal-error">Name is required</mat-error> + } + </mat-form-field> + </div> + <div style="display: flex; flex-direction: column"> + <mat-label class="asterisk--after"> Contact Info</mat-label> + <div style="display: flex; flex-direction: column"> + <mat-form-field> + <input + class="example-full-width" + matInput + formControlName="contact" + /> + @if (authorForm.value.contact) { + <button + matSuffix + mat-icon-button + aria-label="Clear" + (click)="authorForm.controls['contact'].setValue('')" + > + <mat-icon>close</mat-icon> + </button> + } + <mat-hint align="start" class="modal-note example-full-width" + >You can mention Email Address,URL and Phone Number + </mat-hint> + </mat-form-field> + @if ( + authorForm.controls["contact"].hasError("required") && + authorForm.controls["contact"].touched + ) { + <mat-error class="modal-error" + >Contact info is required</mat-error + > + } + </div> + </div> + </div> + <button + color="primary" + style="align-self: end; margin-right: 20px; width: 100px" + mat-raised-button + (click)="onAddAuthorClick(formDirective)" + [disabled]="!authorForm.valid" + > + Add author + </button> + </form> + </div> + </div> +</div> diff --git a/src/app/shared/components/manage-publisher-authors-page/manage-publisher-authors-page.component.scss b/src/app/shared/components/manage-publisher-authors-page/manage-publisher-authors-page.component.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b57625fed110a371378d43cffe51e4419db52553 100644 --- a/src/app/shared/components/manage-publisher-authors-page/manage-publisher-authors-page.component.scss +++ b/src/app/shared/components/manage-publisher-authors-page/manage-publisher-authors-page.component.scss @@ -0,0 +1,42 @@ +.example-full-width { + width: 100%; +} +.purple { + color: #671c9d; +} + +.mat-divider.mat-divider-vertical { + border-right-style: solid; + border-right-color: rgb(246, 243, 250); + border-right-width: 2px; +} + +.modal-note { + color: #888; + font-size: 12px; + font-family: "Open Sans", sans-serif !important; +} + +::ng-deep.mat-mdc-form-field-hint-wrapper, +::ng-deep.mat-mdc-form-field-error-wrapper { + padding: 0px !important; +} + +.asterisk--after:after { + content: "*"; + color: red; +} + +.modal-error { + color: red; + font-size: 12px; + font-family: "Open Sans", sans-serif !important; +} + +.btn-comments-edit { + background: #f1f1f1 url("../../../../assets/images/Comment_edit.png") + no-repeat 3px center; + width: 60px; + margin-left: 4px; + cursor: pointer; +} diff --git a/src/app/shared/components/manage-publisher-authors-page/manage-publisher-authors-page.component.ts b/src/app/shared/components/manage-publisher-authors-page/manage-publisher-authors-page.component.ts index 4fcaff893bf56e986d42627ddcad3c823a3288da..ac48c024f9f3f5db31d308b50b5c97c3ba2c369b 100644 --- a/src/app/shared/components/manage-publisher-authors-page/manage-publisher-authors-page.component.ts +++ b/src/app/shared/components/manage-publisher-authors-page/manage-publisher-authors-page.component.ts @@ -1,13 +1,334 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDividerModule } from '@angular/material/divider'; +import { + AbstractControl, + AsyncValidatorFn, + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + FormsModule, + ReactiveFormsModule, + ValidatorFn, + Validators, +} from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { ActivatedRoute, Router } from '@angular/router'; +import { SharedDataService } from 'src/app/core/services/shared-data/shared-data.service'; +import { PublicSolutionsService } from 'src/app/core/services/public-solutions.service'; +import { PrivateCatalogsService } from 'src/app/core/services/private-catalogs.service'; +import { Alert, AlertType, AuthorPublisherModel } from '../../models'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatIconModule } from '@angular/material/icon'; +import { LiveAnnouncer } from '@angular/cdk/a11y'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { DeleteUserDialogConfirmationActionComponent } from '../delete-user-dialog-confirmation-action/delete-user-dialog-confirmation-action.component'; +import { Observable, map, of } from 'rxjs'; +import { AlertService } from 'src/app/core/services/alert.service'; @Component({ selector: 'gp-manage-publisher-authors-page', standalone: true, - imports: [CommonModule], + imports: [ + CommonModule, + MatButtonModule, + MatDividerModule, + FormsModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule, + MatChipsModule, + MatIconModule, + ], templateUrl: './manage-publisher-authors-page.component.html', - styleUrl: './manage-publisher-authors-page.component.scss' + styleUrl: './manage-publisher-authors-page.component.scss', }) -export class ManagePublisherAuthorsPageComponent { +export class ManagePublisherAuthorsPageComponent implements OnInit { + solutionId!: string; + revisionId!: string; + authorsList!: AuthorPublisherModel[]; + publisherName!: string; + authorForm!: FormGroup; + publisherForm!: FormGroup; + editPublisher: boolean = false; + constructor( + private activatedRoute: ActivatedRoute, + private formBuilder: FormBuilder, + private privateCatalogsService: PrivateCatalogsService, + public dialog: MatDialog, + private alertService: AlertService, + ) { + this.authorForm = this.formBuilder.group({ + name: ['', Validators.required], + contact: ['', Validators.required], + }); + + this.publisherForm = this.formBuilder.group({ + name: [ + '', + [ + Validators.required, + Validators.minLength(2), + Validators.pattern('^[A-Z|a-z|0-9 ]*$'), + // this.notSameAsPublisher(this.publisherName), + ], + // [], + ], + }); + } + + ngOnInit(): void { + this.activatedRoute.parent?.params.subscribe((params) => { + this.solutionId = params['solutionId']; + this.revisionId = params['revisionId']; + this.loadAuthorList(this.solutionId, this.revisionId); + this.loadPublisher(this.solutionId, this.revisionId); + }); + } + + loadAuthorList(solutionId: string, revisionId: string) { + this.privateCatalogsService.getAuthors(solutionId, revisionId).subscribe({ + next: (res) => { + this.authorsList = res; + }, + error: (error) => { + console.error('Error fetching users:', error); + }, + }); + } + + loadPublisher(solutionId: string, revisionId: string) { + this.privateCatalogsService.getPublisher(solutionId, revisionId).subscribe({ + next: (res) => { + if (res) { + this.publisherName = res; + this.publisherNameControlValue = res; + this.updateValidators(); + } + }, + error: (error) => { + console.error('Error fetching users:', error); + }, + }); + } + + updatePublisherData(solutionId: string, revisionId: string) { + this.privateCatalogsService.getPublisher(solutionId, revisionId).subscribe({ + next: (res) => { + this.publisherName = res; + this.publisherNameControlValue = res; + }, + error: (error) => { + console.error('Error fetching users:', error); + }, + }); + } + + onAddAuthorClick(formDirective: FormGroupDirective) { + const author = this.authorForm.value; + + this.privateCatalogsService + .addAuthor(this.solutionId, this.revisionId, author) + .subscribe({ + next: (res) => { + this.authorsList = res; + const alert: Alert = { + message: '', + type: AlertType.Success, + }; + + alert.message = 'Author added successfully. '; + this.alertService.notify(alert, 3000); + this.authorForm.reset(); + formDirective.resetForm(); + }, + error: (error) => { + const alert: Alert = { + message: '', + type: AlertType.Error, + }; + + alert.message = + 'Error while adding Author : ' + error.error.response_detail; + this.alertService.notify(alert, 3000); + }, + }); + } + + onRemoveAuthorClick(author: AuthorPublisherModel) { + const dialogRef: MatDialogRef<DeleteUserDialogConfirmationActionComponent> = + this.dialog.open(DeleteUserDialogConfirmationActionComponent, { + data: { + dataKey: { + title: 'Delete Author', + content: `Would you like to delete author '${author.name}' `, + alertMessage: 'Author deleted successfully. ', + + action: () => this.removeAuthor(author), + }, + }, + autoFocus: false, + }); + + dialogRef.afterClosed().subscribe((result) => { + this.loadAuthorList(this.solutionId, this.revisionId); + }); + } + + OnEditPublisherClick() { + if (this.publisherNameControl) + this.changeAsyncValidationRules( + [this.notSameAsPublisher()], + this.publisherNameControl, + ); + if (this.publisherForm.valid) + this.privateCatalogsService + .updatePublisher( + this.solutionId, + this.revisionId, + this.publisherForm.value.name, + ) + .subscribe({ + next: (res) => { + const alert: Alert = { + message: '', + type: AlertType.Success, + }; + + alert.message = 'Publisher updated successfully. '; + this.alertService.notify(alert, 3000); + + this.updatePublisherData(this.solutionId, this.revisionId); + if (this.publisherNameControl) { + this.clearPublisherNameValidation(this.publisherNameControl); + this.changeSyncValidationRules( + [ + Validators.required, + Validators.minLength(2), + Validators.pattern('^[A-Za-z0-9 ]+$'), + ], + this.publisherNameControl, + ); + } + // this.updatePublisherNameControlValidators(); + /* this.publisherForm.reset(); + formDirective.resetForm(); */ + }, + error: (error) => { + console.log({ error }); + }, + }); + } + + removeAuthor(author: AuthorPublisherModel): Observable<any> { + return this.privateCatalogsService.removeAuthor( + this.solutionId, + this.revisionId, + author ?? null, + ); + } + + onClickEditPublisher() { + this.editPublisher = true; + } + + onCancelClick() { + this.editPublisher = false; + this.publisherForm.setValue({ name: this.publisherName }); + } + + /* notSameAsPublisher(publisherName: string): ValidatorFn { + return (control: AbstractControl): { [key: string]: any } | null => { + console.log('publisher name validation', this.publisherName); + console.log('control.value validation', control.value); + + const isSame = + control.value.trim().toLowerCase() === + publisherName.trim().toLowerCase(); + console.log({ isSame }); + return isSame ? { sameAsPublisher: true } : null; + }; + } + */ + /* notSameAsPublisher(publisherName: string): AsyncValidatorFn { + return ( + control: AbstractControl, + ): Observable<{ [key: string]: any } | null> => { + if (!control.value) { + console.log('inside if'); + return of(null); // If no value, return null immediately + } + + const isSame = + control.value.trim().toLowerCase() === + publisherName.trim().toLowerCase(); + console.log({ isSame }); + + return of(isSame ? { sameAsPublisher: true } : null); + }; + } + */ + notSameAsPublisher(): AsyncValidatorFn { + return ( + control: AbstractControl, + ): Observable<{ [key: string]: any } | null> => { + return of(control.value).pipe( + map((value) => { + const isSame = + value.trim().toLowerCase() === + this.publisherName.trim().toLowerCase(); + return isSame ? { sameAsPublisher: true } : null; + }), + ); + }; + } + + get publisherNameControl() { + return this.publisherForm.get('name'); + } + + get publisherNameControlValue() { + return this.publisherNameControl?.value; + } + + set publisherNameControlValue(value) { + this.publisherNameControl?.setValue(value, { emitEvent: false }); + } + + updateValidators(): void { + const nameControl = this.publisherNameControl; + if (nameControl) { + nameControl.setValidators([ + Validators.required, + Validators.minLength(2), + Validators.pattern('^[A-Za-z0-9 ]+$'), + ]); + nameControl.setAsyncValidators(this.notSameAsPublisher()); + nameControl.updateValueAndValidity({ emitEvent: false }); + } + } + + changeSyncValidationRules( + validators: ValidatorFn | ValidatorFn[] | null, + control: AbstractControl<any, any>, + ): void { + control.setValidators(validators); + } + + changeAsyncValidationRules( + validators: AsyncValidatorFn | AsyncValidatorFn[] | null, + control: AbstractControl<any, any>, + ): void { + control.setAsyncValidators(validators); + // control.addAsyncValidators(validators); + control.updateValueAndValidity(); + } + + clearPublisherNameValidation(control: AbstractControl<any, any>) { + control.clearAsyncValidators(); + } } diff --git a/src/app/shared/components/model-management/model-management.component.html b/src/app/shared/components/model-management/model-management.component.html index 73fa285fc6338f6623422fb53ad8baaa0359e579..caf10af8337522c9f74806f8f3ad752348380a71 100644 --- a/src/app/shared/components/model-management/model-management.component.html +++ b/src/app/shared/components/model-management/model-management.component.html @@ -1,101 +1,195 @@ -<mat-sidenav-container> - <mat-sidenav mode="side" opened> - <div class="workflow-left-header workflow-header">MANAGEMENT OPTIONS</div> +<div + style="display: flex; flex-direction: column" + *ngIf="solution$ | async as data" +> + <div style="display: flex; flex-direction: column; padding: 20px"> <div style=" display: flex; - flex-direction: column !important; - justify-content: center !important; - margin-left: 16px; + flex-direction: row; + align-items: center; + justify-items: center; + gap: 8px; " > - <div> - <span class="tab-box" style="font-size: 14px">On - Boarding</span> - </div> - <div> - <span class="version-on-boarded" *ngIf="selectedRevision?.onBoarded" - >Completed on - {{ selectedRevision.onBoarded | date: "MM/dd/yyyy, h:mm:ss a" }}</span - > + <span class="md-headline5">Manage {{ data.name }}</span + >| + + <gp-version-dropdown + [selectedDefaultRevision]="(selectedRevision$ | async)!" + [revisionsList]="(revisions$ | async)!" + (revisionChange)="onChangeVersion($event)" + ></gp-version-dropdown> + | + <div + style=" + display: flex; + flex-direction: row; + align-items: center; + justify-items: center; + " + *ngIf="sharedWith$ | async as sharedWithData" + > + <span>Author and Publisher - </span> + <img + style="width: 27px; height: 27px" + src="../../../../assets/images/user-profile-black.png" + /> + <div style="display: flex; flex-direction: row"> + <button + mat-icon-button + [matMenuTriggerFor]="menu" + aria-label="Example icon-button with a menu" + > + <mat-icon>more_horiz</mat-icon> + </button> + <mat-menu #menu="matMenu" xPosition="after"> + <mat-list role="list" *ngFor="let item of sharedWithData"> + <mat-list-item role="listitem" style="font-size: 14px"> + <div + style=" + display: flex; + align-items: center; + justify-items: center; + gap: 4px; + " + > + <img + style="width: 27px; height: 27px; display: inline-block" + src="../../../../assets/images/user-profile-black.png" + /> + <span [title]="[item.firstName, item.lastName].join('')" + >{{ + [item.firstName, item.lastName].join(" ") | truncate: 13 + }} + </span> + </div> + </mat-list-item> + </mat-list> + </mat-menu> + </div> </div> </div> - <nav mat-tab-nav-bar class="margin: 20px 0 20px 0;" [tabPanel]="tabPanel"> - <a - mat-tab-link - routerLink="shareWithTeam" - routerLinkActive="is-active" - style="padding-top: 2px; padding-bottom: 8px" + + <div> + <gp-breadcrumb-navigation + [solution]="data" + [firstNavigationLabel]="'Home'" + [secondNavigationLabel]="'Manage my model'" + (firstNavigationClicked)="onHomeClick()" + (secondNavigationClicked)="onManageMyModelClick()" + ></gp-breadcrumb-navigation> + </div> + </div> + + <mat-sidenav-container> + <mat-sidenav mode="side" opened> + <div class="workflow-left-header workflow-header">MANAGEMENT OPTIONS</div> + <div + *ngIf="selectedRevision$ | async as selectedRevision" + style=" + display: flex; + flex-direction: column !important; + justify-content: center !important; + margin-left: 16px; + " > - <mat-icon fontIcon="share"></mat-icon> - <div - style=" - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 2px; - margin-left: 14px; - " + <div> + <span class="tab-box" style="font-size: 14px">On - Boarding</span> + </div> + <div> + <span class="version-on-boarded" + >Completed on + {{ + selectedRevision.onBoarded | date: "MM/dd/yyyy, h:mm:ss a" + }}</span + > + </div> + </div> + <nav mat-tab-nav-bar class="margin: 20px 0 20px 0;" [tabPanel]="tabPanel"> + <a + mat-tab-link + routerLink="shareWithTeam" + routerLinkActive="is-active" + style="padding-top: 2px; padding-bottom: 8px" > - <span class="tab-box" style="font-size: 14px"> Share with team</span> - <span + <mat-icon fontIcon="share"></mat-icon> + <div style=" - margin: 0; - padding: 0; - font-size: 12px; - line-height: 16px; - font-weight: 400; - letter-spacing: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + margin-left: 14px; " - >Shared with {{ sharedWith.length }} co-workers</span > - </div> - </a> - <a mat-tab-link routerLink="publisherAuthors" routerLinkActive="is-active" - ><span - class="managelefticon tabs-left authorsimg" - [ngClass]=" - isActive( - '/dashboard/manageMyModel/' + - solutionId + - revisionId + - '/publisherAuthors' - ) - ? 'authorsimg-active' - : 'authorsimg-inactive' - " - ></span - ><span class="tab-box" style="font-size: 14px" - >Manage Publisher/Authors</span - ></a - > - <a mat-tab-link routerLink="publishModel" routerLinkActive="is-active" - ><span - class="managelefticon tabs-left companyimg" - [ngClass]=" - isActive( - '/dashboard/manageMyModel/' + - solutionId + - revisionId + - '/publishModel' - ) - ? 'companyimg-active' - : 'companyimg-inactive' - " - ></span - ><span class="tab-box" style="font-size: 14px" - >Publish to Marketplace</span - ></a - > - <a mat-tab-link routerLink="deleteModel" routerLinkActive="is-active" - ><mat-icon fontIcon="delete"></mat-icon - ><span - class="tab-box" - style="font-size: 14px; margin-left: 15px !important" - >Delete Model</span - ></a - > - </nav> - <mat-tab-nav-panel #tabPanel></mat-tab-nav-panel> - </mat-sidenav> - <mat-sidenav-content> <router-outlet></router-outlet></mat-sidenav-content -></mat-sidenav-container> + <span class="tab-box" style="font-size: 14px"> + Share with team</span + > + <span + *ngIf="sharedWith$ | async as sharedWithData" + style=" + margin: 0; + padding: 0; + font-size: 12px; + line-height: 16px; + font-weight: 400; + letter-spacing: 0; + " + >Shared with {{ sharedWithData.length }} co-workers</span + > + </div> + </a> + <a + mat-tab-link + routerLink="publisherAuthors" + routerLinkActive="is-active" + ><span + class="managelefticon tabs-left authorsimg" + [ngClass]=" + isActive( + '/dashboard/manageMyModel/' + + solutionId + + revisionId + + '/publisherAuthors' + ) + ? 'authorsimg-active' + : 'authorsimg-inactive' + " + ></span + ><span class="tab-box" style="font-size: 14px" + >Manage Publisher/Authors</span + ></a + > + <a mat-tab-link routerLink="publishModel" routerLinkActive="is-active" + ><span + class="managelefticon tabs-left companyimg" + [ngClass]=" + isActive( + '/dashboard/manageMyModel/' + + solutionId + + revisionId + + '/publishModel' + ) + ? 'companyimg-active' + : 'companyimg-inactive' + " + ></span + ><span class="tab-box" style="font-size: 14px" + >Publish to Marketplace</span + ></a + > + <a mat-tab-link routerLink="deleteModel" routerLinkActive="is-active" + ><mat-icon fontIcon="delete"></mat-icon + ><span + class="tab-box" + style="font-size: 14px; margin-left: 15px !important" + >Delete Model</span + ></a + > + </nav> + <mat-tab-nav-panel #tabPanel></mat-tab-nav-panel> + </mat-sidenav> + <mat-sidenav-content> <router-outlet></router-outlet></mat-sidenav-content + ></mat-sidenav-container> +</div> diff --git a/src/app/shared/components/model-management/model-management.component.scss b/src/app/shared/components/model-management/model-management.component.scss index c59f79ca2f9aed2b1482df0fa66a62a310a3ff33..688aaaf4b5648fe4f3e631c33c503d5074241a51 100644 --- a/src/app/shared/components/model-management/model-management.component.scss +++ b/src/app/shared/components/model-management/model-management.component.scss @@ -148,3 +148,21 @@ mat-sidenav-content { color: #681d9e; font-weight: 700 !important; } + +::ng-deep.mat-mdc-menu-panel { + width: 160px; +} + +.mat-mdc-menu-content { + padding: 0px !important; +} + +.mdc-list { + padding: 0px !important; +} + +.md-headline5 { + font-size: 20px; + font-weight: 600; + color: #671c9d; +} diff --git a/src/app/shared/components/model-management/model-management.component.ts b/src/app/shared/components/model-management/model-management.component.ts index b18e853aed1d2b91ab5c14c83a176025e185e694..156107b41c190618c397704c12e88454c7878421 100644 --- a/src/app/shared/components/model-management/model-management.component.ts +++ b/src/app/shared/components/model-management/model-management.component.ts @@ -10,10 +10,24 @@ import { Revision, UserDetails, } from '../../models'; -import { Subscription } from 'rxjs'; +import { + BehaviorSubject, + Observable, + Subscription, + map, + of, + switchMap, + tap, +} from 'rxjs'; import { SharedDataService } from 'src/app/core/services/shared-data/shared-data.service'; import { PublicSolutionsService } from 'src/app/core/services/public-solutions.service'; import { PrivateCatalogsService } from 'src/app/core/services/private-catalogs.service'; +import { BreadcrumbNavigationComponent } from '../breadcrumb-navigation/breadcrumb-navigation.component'; +import { VersionDropdownComponent } from '../version-dropdown/version-dropdown.component'; +import { MatButtonModule } from '@angular/material/button'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatListModule } from '@angular/material/list'; +import { TruncatePipe } from '../../pipes/truncate.pipe'; @Component({ selector: 'gp-model-management', @@ -24,6 +38,13 @@ import { PrivateCatalogsService } from 'src/app/core/services/private-catalogs.s MatTabsModule, MatIconModule, RouterModule, + BreadcrumbNavigationComponent, + VersionDropdownComponent, + MatButtonModule, + MatMenuModule, + MatIconModule, + MatListModule, + TruncatePipe, ], templateUrl: './model-management.component.html', styleUrl: './model-management.component.scss', @@ -34,6 +55,13 @@ export class ModelManagementComponent implements OnInit { selectedRevision!: Revision; selectedRevisionSubscription!: Subscription; sharedWith: UserDetails[] = []; + solution!: PublicSolutionDetailsModel; + revisionsList!: Revision[]; + solution$!: Observable<PublicSolutionDetailsModel>; + revisions$!: Observable<Revision[]>; + sharedWith$!: Observable<UserDetails[]>; + selectedRevision$: BehaviorSubject<Revision | null> = + new BehaviorSubject<Revision | null>(null); constructor( private router: Router, @@ -43,12 +71,45 @@ export class ModelManagementComponent implements OnInit { private privateCatalogsService: PrivateCatalogsService, ) {} ngOnInit(): void { - this.activatedRoute.params.subscribe((params) => { - this.solutionId = params['solutionId']; - this.revisionId = params['revisionId']; - this.getShareWithTeam(this.solutionId); - this.loadData(this.solutionId, this.revisionId); - }); + this.solution$ = this.activatedRoute.params.pipe( + switchMap((params) => { + this.solutionId = params['solutionId']; + this.revisionId = params['revisionId']; + this.sharedWith$ = this.privateCatalogsService.getShareWithTeam( + this.solutionId, + ); + return this.publicSolutionsService.getSolutionDetails( + this.solutionId, + this.revisionId, + ); + }), + tap((solution) => { + this.revisions$ = this.getRevisionsAsObservable(solution); + const initialRevision = + solution.revisions.find( + (rev) => rev.revisionId === this.revisionId, + ) || solution.revisions[0]; + this.revisions$ = this.getRevisionsAsObservable(solution); + this.updateSelectedRevision(this.revisionId, solution); + this.setRevisionInService(solution.revisions[0]); + }), + ); + } + + private updateSelectedRevision( + revisionId: string, + solution: PublicSolutionDetailsModel, + ): void { + const revision = solution.revisions.find( + (rev) => rev.revisionId === revisionId, + ); + if (revision) { + this.selectedRevision$.next({ + revisionId: revision.revisionId, + version: revision.version, + onBoarded: revision.onboarded, + }); + } } isActive(modelRoute: string): boolean { @@ -59,6 +120,7 @@ export class ModelManagementComponent implements OnInit { this.publicSolutionsService .getSolutionDetails(solutionId, revisionId) .subscribe((res) => { + console.log({ res }); const revisionsList = this.getRevisionsList(res); this.selectedRevision = revisionsList[0]; this.setRevisionInService(this.selectedRevision); @@ -75,14 +137,6 @@ export class ModelManagementComponent implements OnInit { ); } - setRevisionInService(revision: Revision): void { - this.sharedDataService.selectedRevision = { - version: revision.version, - revisionId: revision.revisionId, - onBoarded: revision.onBoarded, - }; - } - getShareWithTeam(solutionId: string): void { this.privateCatalogsService.getShareWithTeam(solutionId).subscribe({ next: (users) => { @@ -93,4 +147,35 @@ export class ModelManagementComponent implements OnInit { }, }); } + onHomeClick() {} + + onManageMyModelClick() {} + + private getRevisionsAsObservable( + solution: PublicSolutionDetailsModel, + ): Observable<Revision[]> { + return new Observable<Revision[]>((subscriber) => { + const revisions = solution.revisions.map((rev) => ({ + version: rev.version, + revisionId: rev.revisionId, + onBoarded: rev.onboarded, + })); + subscriber.next(revisions); + subscriber.complete(); + }); + } + + onChangeVersion(revision: Revision): void { + this.selectedRevision$.next(revision); + this.setRevisionInService(revision); + this.setVersionIdInService(revision.version); + } + + setRevisionInService(revision: Revision): void { + this.sharedDataService.selectedRevision = revision; + } + + setVersionIdInService(version: string): void { + this.sharedDataService.versionId = version; + } } diff --git a/src/app/shared/components/rate-model-details/rate-model-details.component.scss b/src/app/shared/components/rate-model-details/rate-model-details.component.scss index 16612d5286d93d63ec6f024fcc7ea50ff96a4d75..351dc2a9eb7bcb8d592c0705c18794a1882b84ad 100644 --- a/src/app/shared/components/rate-model-details/rate-model-details.component.scss +++ b/src/app/shared/components/rate-model-details/rate-model-details.component.scss @@ -396,7 +396,6 @@ .mdl-button.btn-primary, .mdl-button.btn-secondary { box-shadow: none; - webkit-box-shadow: none; line-height: 27px; text-transform: capitalize; } diff --git a/src/app/shared/components/share-with-team-page/share-with-team-page.component.html b/src/app/shared/components/share-with-team-page/share-with-team-page.component.html index 0c607ef1fab6d3ac77fcb8773aeb89198ebefa28..bc7eb39dd029f63b344816dccf126d3dc3c29381 100644 --- a/src/app/shared/components/share-with-team-page/share-with-team-page.component.html +++ b/src/app/shared/components/share-with-team-page/share-with-team-page.component.html @@ -56,35 +56,6 @@ </span> </div> </div> - - <!-- <div - style="display: flex; flex-direction: row; justify-content: end" - > - <button matChipRemove [attr.aria-label]="'remove ' + sharedUser"> - <mat-icon>cancel</mat-icon> - </button> - </div> - <div - style=" - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - " - > - <img - style="width: 30px; height: 30px" - src="../../../../assets/images/user-profile-black.png" - /> - <span - [title]="[sharedUser.firstName, sharedUser.lastName].join(' ')" - > - {{ - [sharedUser.firstName, sharedUser.lastName].join(" ") - | truncate: 8 - }} - </span> - </div> --> </mat-chip> } </mat-chip-set> @@ -96,7 +67,7 @@ style="display: flex; flex-direction: row; gap: 4px" class="manage-models" > - <span class="purple">Sharing "{{ solution.name }}" </span> + <span class="purple">Sharing "{{ solution?.name }}" </span> |<span> Version - {{ selectedRevision.version }}</span> | <gp-solution-id [solutionId]="solutionId"></gp-solution-id> diff --git a/src/app/shared/components/share-with-team-page/share-with-team-page.component.ts b/src/app/shared/components/share-with-team-page/share-with-team-page.component.ts index ebb19a4ecdbc1dce3cfc65c04f6bab2a2b62ea47..c55ad3a922b5d8552b5074d75bf7290e4916cd34 100644 --- a/src/app/shared/components/share-with-team-page/share-with-team-page.component.ts +++ b/src/app/shared/components/share-with-team-page/share-with-team-page.component.ts @@ -256,8 +256,11 @@ export class ShareWithTeamPageComponent implements OnInit { this.dialog.open(DeleteUserDialogConfirmationActionComponent, { data: { dataKey: { - solutionId: this.solutionId, - user: user, + title: 'Delete Shared Member', + content: `Would you like to delete '${user.firstName} ${user.lastName}' from + the shared list?`, + alertMessage: 'Deleted successfully. ', + action: () => this.deleteUser(user), }, }, autoFocus: false, @@ -267,4 +270,11 @@ export class ShareWithTeamPageComponent implements OnInit { this.getShareWithTeam(this.solutionId); }); } + + deleteUser(user: UserDetails): Observable<any> { + return this.privateCatalogsService.deleteShareWithTeam( + user.userId ?? '', + this.solutionId, + ); + } } diff --git a/src/app/shared/components/version-dropdown/version-dropdown.component.html b/src/app/shared/components/version-dropdown/version-dropdown.component.html new file mode 100644 index 0000000000000000000000000000000000000000..c86f1eced0b1aefe5ec980a6fbb8eb4399b1654a --- /dev/null +++ b/src/app/shared/components/version-dropdown/version-dropdown.component.html @@ -0,0 +1,27 @@ +<div class="version-container"> + <button + class="version-button" + #menuTrigger2="matMenuTrigger" + mat-raised-button + [matMenuTriggerFor]="menu2" + (click)="menuTrigger2.openMenu()" + > + <span>Version - {{ selectedDefaultRevision?.version }}</span> + <mat-icon iconPositionEnd>keyboard_arrow_down</mat-icon> + </button> + <mat-menu #menu2="matMenu"> + <mat-list role="list" *ngFor="let revision of revisionsList"> + <mat-list-item + (click)="onChangeVersion(revision)" + style="cursor: pointer" + [ngClass]="{ + selected: revision.version === selectedDefaultRevision.version + }" + > + <span style="font-size: 14px; line-height: 24px; font-weight: 400" + >Version - {{ revision.version }}</span + > + </mat-list-item> + </mat-list> + </mat-menu> +</div>