diff --git a/src/app/core/config/api-config.ts b/src/app/core/config/api-config.ts index ef0c8f3037eda8812fdcdb19d31dc9136ce46452..025faa86fc8d5f0758f4e7cf997af496c7f3541a 100644 --- a/src/app/core/config/api-config.ts +++ b/src/app/core/config/api-config.ts @@ -65,4 +65,7 @@ export const apiConfig = { urlDeleteTag: '/api/dropTag', urlAddTag: '/api/addTag', urlGetAllTag: '/api/tags', + urlPublishSolution: '/api/publish', + urlSearchPublishRequest: '/api/publish/request/search/revision', + withdrawPublishRequestUrl: '/api/publish/request/withdraw/', }; diff --git a/src/app/core/http-shared/http-shared.service.ts b/src/app/core/http-shared/http-shared.service.ts index cf200e626e810c5090d34a545da62e2fcbe6799e..fc71fad13d0fd1a1c04792c7943dfd80209088d9 100644 --- a/src/app/core/http-shared/http-shared.service.ts +++ b/src/app/core/http-shared/http-shared.service.ts @@ -8,11 +8,18 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; export class HttpSharedService { constructor(private http: HttpClient) {} - private _convertQueryParams(queryParams?: any) { - const params = new HttpParams(); - queryParams.forEach((key: string) => { - params.set(key, queryParams[key]); - }); + private _convertQueryParams(queryParams?: any): HttpParams { + let params = new HttpParams(); + if (queryParams) { + // Iterate over object keys + Object.keys(queryParams).forEach((key) => { + // Check if the property is indeed a property of queryParams + if (queryParams.hasOwnProperty(key)) { + // Add each key to the HttpParams object + params = params.set(key, queryParams[key]); + } + }); + } return params; } diff --git a/src/app/core/services/private-catalogs.service.ts b/src/app/core/services/private-catalogs.service.ts index 3c2d9c258d2ca65e585c710e55d5f6fb2d191368..cc7776af31b3f58eb774396c410580474b768772 100644 --- a/src/app/core/services/private-catalogs.service.ts +++ b/src/app/core/services/private-catalogs.service.ts @@ -11,7 +11,7 @@ import { PublicSolution, PublicSolutionDetailsModel, PublicSolutionsRequestPayload, - Tag, + PublishSolutionRequest, ThreadModel, UserDetails, } from 'src/app/shared/models'; @@ -832,4 +832,67 @@ export class PrivateCatalogsService { }), ); } + + publishSolution( + solutionId: string, + visibility: string, + userId: string, + revisionId: string, + ctlg: string, + ) { + const url = + apiConfig.apiBackendURL + + apiConfig.urlPublishSolution + + '/' + + solutionId + + '?'; + + const data = { + visibility, + userId, + revisionId, + ctlg, + }; + + return this._httpSharedService.put(url, data, undefined).pipe( + catchError((error) => { + throw error; + }), + ); + } + + searchPublishRequestWithCatalogIds( + revisionId: string, + catalogId: string, + ): Observable<PublishSolutionRequest> { + const url = + apiConfig.apiBackendURL + + apiConfig.urlSearchPublishRequest + + '/' + + revisionId + + '/' + + catalogId; + + return this._httpSharedService.get(url, undefined).pipe( + map((res) => res.response_body), + catchError((error) => { + throw error; + }), + ); + } + + withdrawPublishRequest( + publishRequestId: number, + ): Observable<PublishSolutionRequest> { + const url = + apiConfig.apiBackendURL + + apiConfig.withdrawPublishRequestUrl + + publishRequestId; + return this._httpSharedService.put(url).pipe( + map((res) => res.response_body), + catchError((error) => { + throw error; + }), + ); + } } diff --git a/src/app/shared/components/model-details-license-profile/model-details-license-profile.component.ts b/src/app/shared/components/model-details-license-profile/model-details-license-profile.component.ts index 741803576d4d696595ced8dfe8c75bc17c772b0b..c142aca2412239e7505573cfded343ddcc36c031 100644 --- a/src/app/shared/components/model-details-license-profile/model-details-license-profile.component.ts +++ b/src/app/shared/components/model-details-license-profile/model-details-license-profile.component.ts @@ -9,7 +9,11 @@ import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { CreateEditLicenseProfileComponent } from '../create-edit-license-profile/create-edit-license-profile.component'; import { UploadLicenseProfileComponent } from '../upload-license-profile/upload-license-profile.component'; import { BrowserStorageService } from 'src/app/core/services/storage/browser-storage.service'; -import { emptyLicenseProfileModel, LicenseProfileModel } from '../../models'; +import { + emptyLicenseProfileModel, + LicenseProfileModel, + Revision, +} from '../../models'; import { PrivateCatalogsService } from 'src/app/core/services/private-catalogs.service'; @Component({ @@ -28,14 +32,13 @@ export class ModelDetailsLicenseProfileComponent implements OnInit { modelLicenseError = ''; solutionId!: string; revisionId!: string; - versionId: string = ''; - versionIdSubscription: Subscription | undefined; + revisionSubscription: Subscription | undefined; isUserIdAvailable$: Observable<boolean | undefined>; userId$: Observable<string | undefined>; userId!: string; + selectedRevision!: Revision; private subscription: Subscription = new Subscription(); - private destroy$: Subscription = new Subscription(); constructor( private activatedRoute: ActivatedRoute, @@ -71,15 +74,16 @@ export class ModelDetailsLicenseProfileComponent implements OnInit { }); // Get the initial version ID - this.versionId = this.sharedDataService.versionId; + this.selectedRevision = this.sharedDataService.selectedRevision; // Subscribe to changes in versionId - this.versionIdSubscription = this.sharedDataService.versionId$.subscribe( - (versionId: string) => { - this.versionId = versionId; - this.loadData(); - }, - ); + this.revisionSubscription = + this.sharedDataService.selectedRevision$.subscribe( + (revision: Revision) => { + this.selectedRevision = revision; + this.loadData(); + }, + ); // Load data initially this.loadData(); @@ -87,16 +91,16 @@ export class ModelDetailsLicenseProfileComponent implements OnInit { ngOnDestroy() { // Unsubscribe from versionIdSubscription - if (this.versionIdSubscription) { - this.versionIdSubscription.unsubscribe(); + if (this.revisionSubscription) { + this.revisionSubscription.unsubscribe(); } } loadData() { // Check if both solutionId and versionId are available - if (this.solutionId && this.versionId) { + if (this.solutionId && this.selectedRevision.version) { this.publicSolutionsService - .getLicenseFile(this.solutionId, this.versionId) + .getLicenseFile(this.solutionId, this.selectedRevision.version) .subscribe( (res) => { if (res) { @@ -150,7 +154,7 @@ export class ModelDetailsLicenseProfileComponent implements OnInit { this.userId, this.solutionId, this.revisionId, - this.versionId, + this.selectedRevision.version, file, ); } diff --git a/src/app/shared/components/publish-to-marketplace-page/publish-to-marketplace-page.component.html b/src/app/shared/components/publish-to-marketplace-page/publish-to-marketplace-page.component.html index 8fd346a4e8db1d5999a9addcd426f033da08826f..9792b0a36b3ae0718c0dc6c5266f3f18d3ed84a9 100644 --- a/src/app/shared/components/publish-to-marketplace-page/publish-to-marketplace-page.component.html +++ b/src/app/shared/components/publish-to-marketplace-page/publish-to-marketplace-page.component.html @@ -1,12 +1,68 @@ <div class="workflow-right-header workflow-header">Publish to Marketplace</div> <div class="container" *ngIf="data$ | async as data"> <!-- status stepper start--> - <div></div> + <div class="stepper"> + <div class="flex-column"> + <span + [ngClass]=" + statusStepperProgress.documentation + ? 'image-content-active model-documentation-active' + : 'image-content-inactive model-documentation' + " + class="image-content" + ></span> + <span>Model documentation</span> + </div> + <span + [ngClass]=" + statusStepperProgress.requestApproval + ? 'loading-active' + : 'loading-inactive' + " + ></span> + <div class="request-approval-container"> + <span + [ngClass]=" + statusStepperProgress.requestApproval + ? 'request-approval-active request-approval-stepper-active' + : 'image-content-inactive request-approval' + " + class="image-content" + ></span> + <span>Request Approval</span> + <button + *ngIf="statusStepperProgress.requestApproval" + class="withdraw-request-button" + mat-raised-button + (click)="onClickWithdrawRequest()" + > + Withdraw request + </button> + </div> + <span + [ngClass]=" + statusStepperProgress.published ? 'loading-active' : 'loading-inactive' + " + class="loading-inactive" + ></span> + <div class="flex-column"> + <span + [ngClass]=" + statusStepperProgress.published + ? 'loading-active' + : 'yet-not-publish image-content-inactive' + " + class="image-content" + ></span> + <span>Published</span> + </div> + </div> <!-- status stepper end--> <!-- Steps to submit publication start--> <form [formGroup]="publishToMarketPlaceForm" (ngSubmit)="submit()"> <div class="flex-column"> + <span class="form-field-label">Model catalog</span> <mat-form-field> <mat-label>Select a catalog</mat-label> <mat-select @@ -18,203 +74,350 @@ } </mat-select> </mat-form-field> + </div> + <div class="form-container"> + <div class="form-header"> + <span class="form-title">STEPS TO SUBMIT PUBLICATION</span> + <span class="timeline">( COMPLETED)</span> + </div> @if (publishToMarketPlaceForm.controls["catalog"].value) { + <!--Model name starts--> <div class="flex-column"> - <div class="flex-row"> - <mat-form-field> - <mat-label>Model name</mat-label> - <input matInput formControlName="name" /> - </mat-form-field> - <button class="custom-icon" (click)="onClickEditModelName()"> - <mat-icon>edit</mat-icon> - </button> - </div> - - <div class="flex-column"> - <div class="flex-row"> - <span>Model description</span> - <button class="custom-icon" (click)="addEditDescription()"> - @if (publishToMarketPlaceForm.value.description) { - <mat-icon>edit</mat-icon> - } @else { - <mat-icon>add</mat-icon> - } - </button> + <div class="step-container"> + <div class="timeline-container"> + <div class="timeline-entry"> + <span>1</span> + </div> + <mat-divider + vertical + class="divider-color full-height" + ></mat-divider> </div> + <div class="flex-column flex-grow"> + <div class="flex-row"> + <span class="form-field-label">Model name</span> + <button class="custom-icon" (click)="onClickEditModelName()"> + <mat-icon>edit</mat-icon> + </button> + </div> - <div - *ngIf="publishToMarketPlaceForm.value.description" - [innerHTML]="publishToMarketPlaceForm.value.description" - ></div> + <div class="flex-row"> + <mat-form-field> + <mat-label>Model name</mat-label> + <input matInput formControlName="name" /> + </mat-form-field> + </div> + <mat-divider + class="divider-color horizontal-divider" + ></mat-divider> + </div> </div> + <!--Model name ends--> - <div class="flex-column"> - <mat-form-field class="full-width"> - <mat-label>Select category</mat-label> - <mat-select - formControlName="category" - [compareWith]="compareObjects" - > - @for (category of categories; track category) { - <mat-option [value]="category">{{ - category.name - }}</mat-option> - } - </mat-select> - </mat-form-field> - <mat-form-field class="full-width"> - <mat-label>Select toolkitType</mat-label> - - <mat-select - formControlName="toolkitType" - [compareWith]="compareObjects" - > - @for (toolkitType of toolkitTypes; track toolkitType) { - <mat-option [value]="toolkitType">{{ - toolkitType.name - }}</mat-option> - } - </mat-select> - </mat-form-field> + <!--Model description starts--> + <div class="step-container"> + <div class="timeline-container"> + <div class="timeline-entry"> + <span>2</span> + </div> + <mat-divider + vertical + class="divider-color full-height" + ></mat-divider> + </div> + <div class="flex-column flex-grow"> + <div class="flex-row"> + <span class="form-field-label">Model description</span> + <button class="custom-icon" (click)="addEditDescription()"> + @if (publishToMarketPlaceForm.value.description) { + <mat-icon>edit</mat-icon> + } @else { + <mat-icon>add</mat-icon> + } + </button> + </div> + <div + *ngIf="publishToMarketPlaceForm.value.description" + [innerHTML]="publishToMarketPlaceForm.value.description" + ></div> + <div class="flex-column"> + <span class="description-hint" + >The provided description less than 500 characters + </span> + <span class="text-medium" + >If you want to improve your model description rating then + please add more details, else click on 'OK' button to go with + the current rating</span + > + </div> + <mat-divider + class="divider-color horizontal-divider" + ></mat-divider> + </div> </div> - <!--license profile start--> - <div class="flex-column"> - <div class="license-container"> - <span>Model license profile</span> + <!--Model description ends--> - <button class="custom-icon" (click)="onLicenseProfileClick()"> - @if (data.licenseProfile) { - <mat-icon>edit</mat-icon> - } @else { - <mat-icon>add</mat-icon> - } - </button> + <!--Model category starts--> + <div class="step-container"> + <div class="timeline-container"> + <div class="timeline-entry"> + <span>3</span> + </div> + <mat-divider + vertical + class="divider-color full-height" + ></mat-divider> + </div> + <div class="flex-column flex-grow"> + <div class="flex-column"> + <span class="form-field-label">Model category</span> + <mat-form-field class="full-width"> + <mat-label>Select category</mat-label> + <mat-select + formControlName="category" + [compareWith]="compareObjects" + > + @for (category of categories; track category) { + <mat-option [value]="category">{{ + category.name + }}</mat-option> + } + </mat-select> + </mat-form-field> + </div> + <mat-form-field class="full-width"> + <mat-label>Select toolkitType</mat-label> + <mat-select + formControlName="toolkitType" + [compareWith]="compareObjects" + > + @for (toolkitType of toolkitTypes; track toolkitType) { + <mat-option [value]="toolkitType">{{ + toolkitType.name + }}</mat-option> + } + </mat-select> + </mat-form-field> + <mat-divider + class="divider-color horizontal-divider" + ></mat-divider> </div> </div> - @if (addEditLicenseProfile) { - <div class="flex-row full-width"> - <gp-model-details-license-profile - [isExistingLicenseProfile]="true" - ></gp-model-details-license-profile> + <!--Model category ends--> + + <!--license profile start--> + <div class="step-container"> + <div class="timeline-container"> + <div class="timeline-entry"> + <span>4</span> + </div> + <mat-divider + vertical + class="divider-color full-height" + ></mat-divider> </div> - } + <div class="flex-column flex-grow"> + <div class="license-container"> + <div class="flex-row"> + <span class="form-field-label">Model license profile</span> + <button class="custom-icon" (click)="onLicenseProfileClick()"> + @if (data.licenseProfile) { + <mat-icon>edit</mat-icon> + } @else { + <mat-icon>add</mat-icon> + } + </button> + </div> + <input + style="width: 20%" + *ngIf="licenseName" + [value]="licenseName" + disabled + /> + </div> + </div> + @if (addEditLicenseProfile) { + <div class="flex-row full-width spacebetween buttons-layout"> + <button + mat-stroked-button + color="primary" + (click)="onClickUploadLicenseProfile()" + > + Upload + </button> + <button + *ngIf="data.licenseProfile" + mat-stroked-button + color="primary" + (click)="onClickUpdateLicenseProfile()" + > + Update + </button> + <button + *ngIf="!data.licenseProfile" + mat-stroked-button + (click)="onClickCreateLicenseProfile()" + > + Create + </button> + </div> + } + <mat-divider class="divider-color horizontal-divider"></mat-divider> + </div> <!--license profile end--> - <!--upload documents start--> - <div class="upload-file-container"> - <div class="flex-row"> - <span class="upload-file-text">Model Documents </span> - <button class="custom-icon" (click)="onClickUploadDocumentFile()"> - @if (documents().length > 0) { - <mat-icon>edit</mat-icon> - } @else { - <mat-icon>add</mat-icon> - } - </button> + <!--upload documents start--> + <div class="step-container"> + <div class="timeline-container"> + <div class="timeline-entry"> + <span>5</span> + </div> + <mat-divider + vertical + class="divider-color full-height" + ></mat-divider> </div> + <div class="upload-file-container flex-grow"> + <div class="flex-row"> + <span class="form-field-label">Model Documents </span> - <mat-form-field> - <mat-chip-grid #documentsGrid formControlName="documents"> - @for (document of documents(); track document) { - <mat-chip-row (removed)="removeDocument(document)"> - {{ document.name }} - <button - matChipRemove - [attr.aria-label]="'remove ' + document" - > - <mat-icon>cancel</mat-icon> - </button> - </mat-chip-row> - } - </mat-chip-grid> - <input - matInput - [matChipInputFor]="documentsGrid" - (matChipInputTokenEnd)="onAddEditDocuments($event)" - /> - </mat-form-field> + <button + class="custom-icon" + (click)="onClickUploadDocumentFile()" + > + @if (documents().length > 0) { + <mat-icon>edit</mat-icon> + } @else { + <mat-icon>add</mat-icon> + } + </button> + </div> + + <mat-form-field> + <mat-chip-grid #documentsGrid formControlName="documents"> + @for (document of documents(); track document) { + <mat-chip-row (removed)="removeDocument(document)"> + {{ document.name }} + <button + matChipRemove + [attr.aria-label]="'remove ' + document" + > + <mat-icon>cancel</mat-icon> + </button> + </mat-chip-row> + } + </mat-chip-grid> + <input + matInput + [matChipInputFor]="documentsGrid" + (matChipInputTokenEnd)="onAddEditDocuments($event)" + /> + </mat-form-field> + <div class="flex-column flex-grow"> + <span class="text-medium" + >Add documents (such as a README file) to give more details + and show how to use your model.</span + > + </div> + <mat-divider + class="divider-color horizontal-divider" + ></mat-divider> + </div> </div> <!--upload documents end--> + <!--tags starts--> - <mat-form-field class="flex-grow"> - <mat-label>Add tag</mat-label> - <mat-chip-grid #chipGrid> - @for (tag of tagsItems(); track tag) { - <mat-chip-row (removed)="removeTag(tag)"> - {{ tag.tag }} - <button matChipRemove [attr.aria-label]="'remove ' + tag"> - <mat-icon>cancel</mat-icon> - </button> - </mat-chip-row> - } - </mat-chip-grid> - <input - #tagInput - [formControl]="tagCtrl" - [matChipInputFor]="chipGrid" - [matAutocomplete]="auto" - [matChipInputSeparatorKeyCodes]="separatorKeysCodes" - (matChipInputTokenEnd)="addTag($event)" - (input)="onChangeInput($event)" - /> - <mat-autocomplete - #auto="matAutocomplete" - (optionSelected)="selected($event)" - > - @for (tag of filteredTags | async; track tag) { - <mat-option [value]="tag" - ><span class="autocomplete-option">{{ - tag - }}</span></mat-option + <div class="step-container"> + <div class="timeline-container"> + <div class="timeline-entry"> + <span>6</span> + </div> + <mat-divider + vertical + class="divider-color full-height" + ></mat-divider> + </div> + <div class="flex-column flex-grow"> + <span class="form-field-label">Model tags</span> + <mat-form-field class="flex-grow"> + <mat-label>Add tag</mat-label> + <mat-chip-grid #chipGrid> + @for (tag of tagsItems(); track tag) { + <mat-chip-row (removed)="removeTag(tag)"> + {{ tag.tag }} + <button matChipRemove [attr.aria-label]="'remove ' + tag"> + <mat-icon>cancel</mat-icon> + </button> + </mat-chip-row> + } + </mat-chip-grid> + <input + #tagInput + [formControl]="tagCtrl" + [matChipInputFor]="chipGrid" + [matAutocomplete]="auto" + [matChipInputSeparatorKeyCodes]="separatorKeysCodes" + (matChipInputTokenEnd)="addTag($event)" + (input)="onChangeInput($event)" + /> + <mat-autocomplete + #auto="matAutocomplete" + (optionSelected)="selected($event)" > - } - </mat-autocomplete> - </mat-form-field> + @for (tag of filteredTags | async; track tag) { + <mat-option [value]="tag" + ><span class="autocomplete-option">{{ + tag + }}</span></mat-option + > + } + </mat-autocomplete> + </mat-form-field> + <mat-divider + class="divider-color horizontal-divider" + ></mat-divider> + </div> + </div> <!--tags ends--> - <!--upload image start--> - <div class="flex-column"> - <span class="upload-file-text">Upload image model </span> - <p class="upload-text-hint"> - Upload an image that will identify your model in Marketplace. You - can upload jpg, jpeg, png and gif file with maximum size of 1MB. - </p> - <div class="single-file"> - <div class="image-container"> - <img class="file-image" [src]="imageToShow" alt="file" /> - </div> - <div class="flex-row file-name"> - <span *ngIf="imageToShow.name"> {{ imageToShow.name }}</span> - <span>|</span> - <a (click)="onClickUploadImageFile()">Change</a> + <!--upload image start--> + <div class="step-container"> + <div class="timeline-container"> + <div class="timeline-entry"> + <span>7</span> </div> </div> + <div class="flex-column"> + <div class="flex-row"> + <span class="form-field-label">Model image </span> - <!-- <div class="file-name"> - | <a ng-click="showImageUpload = !showImageUpload">Change</a> - <!- - <span>Size 800K</span> - -> - </div> --> - <!-- <mat-form-field class="mat-form-field-upload-file"> - <input matInput /> + @if (!imageToShow) { + <button + class="custom-icon" + (click)="onClickUploadImageFile()" + > + <mat-icon>upload</mat-icon> + </button> + } + </div> + <p class="upload-text-hint"> + Upload an image that will identify your model in Marketplace. + You can upload jpg, jpeg, png and gif file with maximum size of + 1MB. + </p> - <input - #fileDropRef - type="file" - id="fileInput" - name="fileInput" - (change)="selectImageFile($event)" - hidden - formControlName="image" - /> - </mat-form-field> --> - <!-- <button class="custom-icon" (click)="onClickUploadImageFile()"> - @if (imageToShow) { - <mat-icon>edit</mat-icon> - } @else { - <mat-icon>add</mat-icon> - } - </button> --> + <div *ngIf="imageToShow" class="single-file"> + <div class="image-container"> + <img class="file-image" [src]="imageToShow" alt="file" /> + </div> + <div class="flex-row file-name"> + <span *ngIf="imageToShow.name"> {{ imageToShow?.name }}</span> + <span>|</span> + <a (click)="onClickUploadImageFile()">Change</a> + </div> + </div> + </div> </div> <!--upload image end--> </div> @@ -233,7 +436,7 @@ mat-raised-button color="primary" type="submit" - [disabled]="publishToMarketPlaceForm.invalid" + [disabled]="!enableSubmit" > Publish model </button> diff --git a/src/app/shared/components/publish-to-marketplace-page/publish-to-marketplace-page.component.scss b/src/app/shared/components/publish-to-marketplace-page/publish-to-marketplace-page.component.scss index c0a41a5b001e0f5378a3eb429cf83d0159f09e17..387aef2fe7da1fb0721486d3198283112fee88d7 100644 --- a/src/app/shared/components/publish-to-marketplace-page/publish-to-marketplace-page.component.scss +++ b/src/app/shared/components/publish-to-marketplace-page/publish-to-marketplace-page.component.scss @@ -6,12 +6,17 @@ border: none !important; color: mat.get-color-from-palette($graphene-ui-primary); cursor: pointer; + + mat-icon { + font-size: 16px !important; + } } .container { display: flex; flex-direction: column; padding: 40px; + height: 100%; } .flex-column { @@ -36,8 +41,8 @@ .license-container { display: flex; - align-items: center; - gap: 40px; + flex-direction: column; + gap: 10px; } .upload-file-container { @@ -46,10 +51,6 @@ gap: 20px; } -.upload-file-text { - width: 20%; -} - .mat-form-field-upload-file { height: 70px; flex-grow: 1; @@ -76,11 +77,12 @@ gap: 20px; button { - width: 20%; + width: 30%; } } .single-file { + width: 20%; display: flex; padding: 0.5rem; align-items: center; @@ -101,8 +103,8 @@ .file-image { align-self: flex-start; border: 1px solid #dbcbe8; - width: 24px; - height: 24px; + width: 40px; + height: 40px; padding: 3px; } @@ -144,3 +146,192 @@ hyphens: auto; color: #666; } + +.form-title { + font-size: 16px; + text-transform: uppercase; + font-weight: bold; + color: mat.get-color-from-palette($graphene-ui-primary); +} + +.timeline { + font-size: 14px; + font-weight: normal; + color: mat.get-color-from-palette($graphene-ui-primary); +} + +.form-header { + border-bottom: 1px solid #ddd; + padding-bottom: 4px; + margin-bottom: 24px; +} + +.image-content { + width: 74px; + height: 74px; + border-radius: 74px; + float: left; + position: relative; + text-align: center; + line-height: 68px; +} + +.image-content-inactive { + border: 3px solid #fff; + background-color: #ebebeb; +} + +.image-content-active { + border: 3px solid #009901; + background-color: #fff; +} + +.request-approval-stepper-active { + border: 3px solid #671c9d; + background-color: #fff; +} + +.model-documentation { + background-image: url(../../../../assets/images/ico_model_documentation_grey.png); + background-repeat: no-repeat; + background-position: center; +} + +.model-documentation-active { + background-image: url(../../../../assets/images/ico-model-documentation-green.png); + background-repeat: no-repeat; + background-position: center; +} + +.request-approval { + background-image: url(../../../../assets/images/request_approval.png); + background-repeat: no-repeat; + background-position: center; +} + +.request-approval-active { + background-image: url(../../../../assets/images/request_approval_selected.png); + background-repeat: no-repeat; + background-position: center; +} + +.yet-not-publish { + background-image: url(../../../../assets/images/cloud.svg); + background-repeat: no-repeat; + background-position: center; +} + +.stepper { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; +} + +.loading-inactive { + background: url("../../../../assets/images/loading_deactive.png") no-repeat + left center; + width: 45px; + height: 12px; +} + +.loading-active { + background: url("../../../../assets/images/stepper_progress_completed.png") + no-repeat left center; + width: 45px; + height: 12px; +} + +.withdraw-request-button { + background-color: transparent; + border: 1px solid #671c9d; + height: 28px !important; +} + +.request-approval-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} + +.buttons-layout { + margin: 10px; + gap: 10px; +} + +.spacebetween { +} + +.description-hint { + color: #666; + font-size: 12px; + font-family: "Open Sans", sans-serif; + font-weight: 600; +} +.text-medium { + font-size: 13px !important; + font-weight: 400; + line-height: 24px; + font-family: "Open Sans", sans-serif; + color: #666; +} + +.form-field-label { + font-size: 14px; + font-weight: bold; + color: #671c9d; + text-decoration: none; + cursor: pointer; +} + +.divider-color { + color: #dbcbe8; +} + +.timeline-entry { + background: #fff; + color: #212121; + display: block; + width: 18px; + height: 18px; + -webkit-background-clip: padding-box; + background-clip: padding-box; + -webkit-border-radius: 18px; + border-radius: 18px; + text-align: center; + -webkit-box-shadow: 0 0 0 2px #671c9d; + box-shadow: 0 0 0 2px #671c9d; + line-height: 18px; + font-size: 12px; + float: left; +} + +.step-container { + display: flex; + flex-direction: row; + gap: 20px; +} + +.timeline-container { + display: flex; + flex-direction: column; + align-items: center; +} + +.full-height { + height: 100%; +} + +.horizontal-divider { + margin-left: 6px; + margin-top: 30px; + margin-bottom: 40px; +} + +.form-fields-layout { + display: flex; + flex-direction: column; + gap: 20px; +} diff --git a/src/app/shared/components/publish-to-marketplace-page/publish-to-marketplace-page.component.ts b/src/app/shared/components/publish-to-marketplace-page/publish-to-marketplace-page.component.ts index c4671ce626a887666fe0aa720f9e37070edc4afa..7bf22595dd24230d0d74d4c61c9f4caae7a7fffa 100644 --- a/src/app/shared/components/publish-to-marketplace-page/publish-to-marketplace-page.component.ts +++ b/src/app/shared/components/publish-to-marketplace-page/publish-to-marketplace-page.component.ts @@ -15,9 +15,10 @@ import { Catalog, CatalogFilter, DocumentModel, - Filter, LicenseProfileModel, PublicSolutionDetailsModel, + PublicSolutionDetailsRevisionModel, + PublishSolutionRequest, Revision, Tag, ToolkitTypeCode, @@ -57,7 +58,10 @@ import { Observable, of, startWith, + Subject, + Subscription, switchMap, + takeUntil, tap, } from 'rxjs'; import { UpdateModelNameDialogComponent } from '../update-model-name-dialog/update-model-name-dialog.component'; @@ -67,6 +71,10 @@ import { MatAutocompleteSelectedEvent, } from '@angular/material/autocomplete'; import { COMMA, ENTER } from '@angular/cdk/keycodes'; +import { BrowserStorageService } from 'src/app/core/services/storage/browser-storage.service'; +import { CreateEditLicenseProfileComponent } from '../create-edit-license-profile/create-edit-license-profile.component'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatDividerModule } from '@angular/material/divider'; interface RouteParams { solutionId: string; revisionId: string; @@ -104,6 +112,7 @@ interface ModelData { MatChipsModule, MatIconModule, MatAutocompleteModule, + MatDividerModule, ], templateUrl: './publish-to-marketplace-page.component.html', styleUrl: './publish-to-marketplace-page.component.scss', @@ -144,21 +153,20 @@ export class PublishToMarketplacePageComponent implements OnInit { defaultSolutionData = {}; imageToShow: any; readonly separatorKeysCodes: number[] = [ENTER, COMMA]; - - createImageFromBlob(image: Blob) { - const reader = new FileReader(); - reader.addEventListener( - 'load', - () => { - this.imageToShow = reader.result; - }, - false, - ); - - if (image) { - reader.readAsDataURL(image); - } - } + private subscription: Subscription = new Subscription(); + userId!: string; + revisionsList!: Revision[]; + selectedDefaultRevision!: Revision; + licenseName!: string; + statusStepperProgress = { + documentation: false, + requestApproval: false, + published: false, + }; + publishRequest!: PublishSolutionRequest; + isSubmitted = false; + enableSubmit = false; + private onDestroy = new Subject<void>(); constructor( private activatedRoute: ActivatedRoute, @@ -168,7 +176,7 @@ export class PublishToMarketplacePageComponent implements OnInit { private sharedDataService: SharedDataService, public dialog: MatDialog, private alertService: AlertService, - + private browserStorageService: BrowserStorageService, private publicSolutionsService: PublicSolutionsService, ) { this.buildForm(); @@ -178,6 +186,15 @@ export class PublishToMarketplacePageComponent implements OnInit { return this.publicSolutionsService .getSolutionDetails(solutionId, revisionId) .pipe( + tap((res) => { + this.revisionsList = this.getRevisionsList(res); + + this.selectedDefaultRevision = this.revisionsList.filter( + (rv) => rv.revisionId === this.revisionId, + )[0]; + + this.setRevisionInService(this.selectedDefaultRevision); + }), switchMap((solution) => { const documentStream = this.selectedCatalog && this.selectedCatalog.catalogId @@ -206,7 +223,10 @@ export class PublishToMarketplacePageComponent implements OnInit { .getPictureOfSolution(solutionId) .pipe(catchError(() => of(null))), licenseProfile: this.publicSolutionsService - .getLicenseFile(solutionId, revisionId) + .getLicenseFile( + solutionId, + this.selectedDefaultRevision.version, + ) .pipe(catchError(() => of(null))), tags: this.privateCatalogsService.getAllTag(), documents: documentStream, @@ -244,7 +264,7 @@ export class PublishToMarketplacePageComponent implements OnInit { category: [null, [Validators.required]], toolkitType: [null, Validators.required], licenseProfile: [null, [Validators.required]], - documents: [[], [Validators.required]], + documents: [[]], tags: [[], [Validators.required]], image: [null, [Validators.required]], }); @@ -254,6 +274,25 @@ export class PublishToMarketplacePageComponent implements OnInit { this.activatedRoute.parent?.params.subscribe((params) => { this.solutionId = params['solutionId']; this.revisionId = params['revisionId']; + this.privateCatalogsService + .getAuthors(this.solutionId, this.revisionId) + .subscribe({ + next: (res) => { + console.log({ res }); + if (res.length === 0) { + const alert: Alert = { + message: + 'You cannot publish the model without entering the author name. Please add author name in the "Manage Publisher/Authors" page to publish it.', + type: AlertType.Error, + }; + this.alertService.notify(alert); + this.alertService.notify; + } + }, + error: (error) => { + console.error('Error fetching users:', error); + }, + }); this.publicSolutionsService .getSolutionDetails(this.solutionId, this.revisionId) .subscribe({ @@ -302,6 +341,40 @@ export class PublishToMarketplacePageComponent implements OnInit { .subscribe((filteredTags) => { this.filteredTags.next(filteredTags); }); + + this.subscription.add( + this.browserStorageService.getUserDetails().subscribe((details) => { + this.userId = details?.userId ?? ''; + }), + ); + this.publishToMarketPlaceForm.valueChanges + .pipe(takeUntil(this.onDestroy)) + .subscribe(() => { + this.checkFormValidity(); + }); + } + + checkFormValidity() { + const { + name, + catalog, + description, + category, + toolkitType, + licenseProfile, + tags, + image, + } = this.publishToMarketPlaceForm.value; + + this.enableSubmit = + name && + catalog && + description && + category && + toolkitType && + licenseProfile && + tags && + image; } private filterTags(inputValue: string, tags: string[]): string[] { @@ -338,6 +411,7 @@ export class PublishToMarketplacePageComponent implements OnInit { this.publishToMarketPlaceForm.patchValue({ description: res.response_body.description, }); + console.log('description', res.response_body.description); }, error: (error) => { console.error('Failed to fetch description', error); @@ -366,10 +440,55 @@ export class PublishToMarketplacePageComponent implements OnInit { console.error('Failed to fetch documents', error); }, }); + + this.privateCatalogsService + .searchPublishRequestWithCatalogIds( + this.revisionId, + this.selectedCatalog.catalogId, + ) + .subscribe({ + next: (res) => { + this.publishRequest = res; + if (res) { + if (res.requestStatusCode === 'PE') { + this.statusStepperProgress.documentation = true; + this.statusStepperProgress.requestApproval = true; + } + if (res.requestStatusCode === 'AP') { + this.statusStepperProgress.documentation = true; + this.statusStepperProgress.published = true; + } + if (res.requestStatusCode === 'WD') { + this.statusStepperProgress.requestApproval = false; + this.statusStepperProgress.published = false; + } + } + }, + error: (error) => { + console.error('Failed to fetch publish request', error); + // Handle the error, e.g., set a default value or show an error message + }, + }); } } - submit() {} + submit() { + this.privateCatalogsService + .publishSolution( + this.solutionId, + this.selectedCatalog.accessTypeCode, + this.userId, + this.revisionId, + this.selectedCatalog.catalogId, + ) + .subscribe({ + next: (res) => { + console.log({ res }); + this.loadPublishRequest(); + }, + error: () => {}, + }); + } created(event: any) {} @@ -468,6 +587,7 @@ export class PublishToMarketplacePageComponent implements OnInit { action: (file: File) => this.onClickUpdateSolutionDocument(file), isLicenseProfile: false, isProcessEvent: false, + accept: 'application/pdf', }, }, }); @@ -643,7 +763,10 @@ export class PublishToMarketplacePageComponent implements OnInit { this.revisionId = data.revisionId; this.solutionId = data.solutionId; - if (data.picture) this.createImageFromBlob(data.picture); + if (data.picture) { + this.createImageFromBlob(data.picture); + } + if (data.solution?.modelTypeName && data.solution.modelType) { this.publishToMarketPlaceForm.get('category')?.setValue({ name: data.solution?.modelTypeName, @@ -663,7 +786,7 @@ export class PublishToMarketplacePageComponent implements OnInit { if ( data.solution && - data.solution?.solutionTagList.length && + data.solution?.solutionTagList && data.solution.solutionTagList.length >= 1 ) { const tagsList = data.solution?.solutionTagList; @@ -692,6 +815,18 @@ export class PublishToMarketplacePageComponent implements OnInit { const updatedTags = data.tags ?? []; this.allTags.update((tags: string[]) => [...updatedTags]); } + + if (data.licenseProfile) { + this.licenseName = + 'license-' + this.selectedDefaultRevision.version + '.json'; + this.publishToMarketPlaceForm.patchValue({ + licenseProfile: data.licenseProfile, + }); + } + } + + setRevisionInService(revision: Revision): void { + this.sharedDataService.selectedRevision = revision; } editModelName(name: string) { @@ -751,4 +886,184 @@ export class PublishToMarketplacePageComponent implements OnInit { error: () => {}, }); } + + uploadNewLicenseFile(file: File) { + return this.privateCatalogsService.uploadNewModelLicenseProfile( + this.userId, + file, + ); + } + + uploadExistingLicenseFile(file: File): Observable<any> { + console.log('revisionId', this.revisionId); + console.log('selectedDefaultRevision', this.selectedDefaultRevision); + return this.privateCatalogsService.uploadExistingModelLicenseProfile( + this.userId, + this.solutionId, + this.selectedDefaultRevision.revisionId, + this.selectedDefaultRevision.version, + file, + ); + } + + onClickUploadLicenseProfile() { + const dialogRef: MatDialogRef<UploadLicenseProfileComponent> = + this.dialog.open(UploadLicenseProfileComponent, { + data: { + dataKey: { + isEditMode: false, + title: 'Upload License file', + errorMessage: + 'Please update the license profile to correct the following validation errors:', + supportedFileText: + 'Maximum file size: 1mb | Supported file type: .json', + action: (file: File) => this.uploadExistingLicenseFile(file), + isLicenseProfile: true, + isProcessEvent: true, + }, + }, + }); + + dialogRef.afterClosed().subscribe((result) => { + // This will be executed when the dialog is closed + // Reload data to fetch the updated license profile + }); + } + + getRevisionsList(solution: PublicSolutionDetailsModel): Revision[] { + return solution.revisions.map( + (revision: PublicSolutionDetailsRevisionModel) => ({ + version: revision.version, + revisionId: revision.revisionId, + onBoarded: revision.onboarded, + }), + ); + } + + createImageFromBlob(image: Blob) { + const reader = new FileReader(); + reader.addEventListener( + 'load', + () => { + this.imageToShow = reader.result; + this.publishToMarketPlaceForm.patchValue({ + image: reader.result, + }); + }, + false, + ); + + if (image) { + reader.readAsDataURL(image); + } + } + + onClickUpdateLicenseProfile() { + const dialogRef: MatDialogRef<CreateEditLicenseProfileComponent> = + this.dialog.open(CreateEditLicenseProfileComponent, { + data: { + dataKey: { + modelLicense: null, + solutionId: this.solutionId, + revisionId: this.revisionId, + isEditMode: true, + }, + }, + }); + + dialogRef.afterClosed().subscribe((result) => { + // This will be executed when the dialog is closed + // Reload data to fetch the updated license profile + }); + } + + onClickCreateLicenseProfile() { + const dialogRef: MatDialogRef<CreateEditLicenseProfileComponent> = + this.dialog.open(CreateEditLicenseProfileComponent, { + data: { + dataKey: { + isEditMode: false, + solutionId: this.solutionId, + revisionId: this.revisionId, + }, + }, + }); + dialogRef.afterClosed().subscribe((result) => { + // This will be executed when the dialog is closed + // Reload data to fetch the updated license profile + }); + } + + onClickWithdrawRequest() { + this.privateCatalogsService + .withdrawPublishRequest(this.publishRequest.publishRequestId) + .subscribe({ + next: (res) => { + this.publishRequest = res; + if (res.requestStatusCode == 'PE') { + this.statusStepperProgress.documentation = true; + this.statusStepperProgress.requestApproval = true; + } + if (res.requestStatusCode === 'AP') { + this.statusStepperProgress.documentation = true; + this.statusStepperProgress.published = true; + } + if (res.requestStatusCode === 'WD') { + this.statusStepperProgress.requestApproval = false; + this.statusStepperProgress.published = false; + } + }, + error: (error) => {}, + }); + } + + loadPublishRequest() { + this.publicSolutionsService + .getSolutionDocuments( + this.solutionId, + this.revisionId, + this.selectedCatalog.catalogId, + ) + .subscribe({ + next: (res) => { + this.publishToMarketPlaceForm.patchValue({ + documents: res, + }); + + this.documents.update((documents: DocumentModel[]) => [ + ...documents, + ...res, + ]); + }, + error: (error) => { + console.error('Failed to fetch documents', error); + }, + }); + this.privateCatalogsService + .searchPublishRequestWithCatalogIds( + this.revisionId, + this.selectedCatalog.catalogId, + ) + .subscribe({ + next: (res) => { + res = this.publishRequest; + if (res.requestStatusCode == 'PE') { + this.statusStepperProgress.documentation = true; + this.statusStepperProgress.requestApproval = true; + } + if (res.requestStatusCode === 'AP') { + this.statusStepperProgress.documentation = true; + this.statusStepperProgress.published = true; + } + if (res.requestStatusCode === 'WD') { + this.statusStepperProgress.requestApproval = false; + this.statusStepperProgress.published = false; + } + }, + error: (error) => { + console.error('Failed to fetch description', error); + // Handle the error, e.g., set a default value or show an error message + }, + }); + } } diff --git a/src/app/shared/components/upload-license-profile/upload-license-profile.component.html b/src/app/shared/components/upload-license-profile/upload-license-profile.component.html index 694b5560192d2a47ae02291ca9f9be1f59fc52a7..0a8cc527d9f5684b417becc5c430bfc31c8c1dc7 100644 --- a/src/app/shared/components/upload-license-profile/upload-license-profile.component.html +++ b/src/app/shared/components/upload-license-profile/upload-license-profile.component.html @@ -30,6 +30,7 @@ class="upload-txtbox" id="fileDropRef" (change)="handleFileInput($event)" + [accept]="accept" /> Browse for file </button> diff --git a/src/app/shared/components/upload-license-profile/upload-license-profile.component.ts b/src/app/shared/components/upload-license-profile/upload-license-profile.component.ts index 8a2c81ab3871554cd9942e69e831a31988156353..d24978b264a191123ad83eb79b4f4da1f3388476 100644 --- a/src/app/shared/components/upload-license-profile/upload-license-profile.component.ts +++ b/src/app/shared/components/upload-license-profile/upload-license-profile.component.ts @@ -83,6 +83,7 @@ export class UploadLicenseProfileComponent implements OnInit, OnDestroy { supportedFileText!: string; title!: string; isProcessEvent!: boolean; + accept!: string; constructor( private alertService: AlertService, @@ -115,6 +116,7 @@ export class UploadLicenseProfileComponent implements OnInit, OnDestroy { this.supportedFileText = this.data.dataKey.supportedFileText; this.title = this.data.dataKey.title; this.isProcessEvent = this.data.dataKey.isProcessEvent; + this.accept = this.data.dataKey.accept; // Get the initial version ID this.versionId = this.sharedDataService.versionId; diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index 081bcc5a4f45ca2eb51eddc18ec8ba13da2b3fb4..e3fdb9fae0c8c6648282d0768f2fe13fca8703a6 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -6,3 +6,4 @@ export * from './comment'; export * from './navigation-label.model'; export * from './empty-models.model'; export * from './document.model'; +export * from './publish-request.model'; diff --git a/src/app/shared/models/publish-request.model.ts b/src/app/shared/models/publish-request.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..4123ab9a399ac8d9e05384588a5d0801c2576ccb --- /dev/null +++ b/src/app/shared/models/publish-request.model.ts @@ -0,0 +1,21 @@ +export interface PublishSolutionRequest { + publishRequestId: number; + solutionId: string; + revisionId: string; + requestUserId: string; + requestUserName: string; + approverId: string; + requestStatusCode: string; + comment: string; + requestorName: string; + solutionName: string; + revisionName: string; + revisionStatusCode: string; + revisionStatusName: string; + requestStatusName: string; + creationDate: string; + lastModifiedDate: string; + catalogId: string; + catalogName: string; + accessType: string; +} diff --git a/src/assets/images/cloud-active.svg b/src/assets/images/cloud-active.svg new file mode 100644 index 0000000000000000000000000000000000000000..638ee54e4b019b200e2625b45213fece95ff98ce --- /dev/null +++ b/src/assets/images/cloud-active.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#009901"><path d="M240-192q-80 0-136-56T48-384q0-76 52-131.5T227-576q23-85 92.5-138.5T480-768q103 0 179 69.5T744-528q70 0 119 49t49 119q0 70-49 119t-119 49H240Zm0-72h504q40 0 68-28t28-68q0-40-28-68t-68-28h-66l-6-65q-7-74-62-124.5T480-696q-64 0-115 38.5T297-556l-14 49-51 3q-48 3-80 37.5T120-384q0 50 35 85t85 35Zm240-216Z"/></svg> \ No newline at end of file diff --git a/src/assets/images/cloud.svg b/src/assets/images/cloud.svg new file mode 100644 index 0000000000000000000000000000000000000000..384744e13b5c98025ac9ba3e68c0f07e8516338f --- /dev/null +++ b/src/assets/images/cloud.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#666666"><path d="M240-192q-80 0-136-56T48-384q0-76 52-131.5T227-576q23-85 92.5-138.5T480-768q103 0 179 69.5T744-528q70 0 119 49t49 119q0 70-49 119t-119 49H240Zm0-72h504q40 0 68-28t28-68q0-40-28-68t-68-28h-66l-6-65q-7-74-62-124.5T480-696q-64 0-115 38.5T297-556l-14 49-51 3q-48 3-80 37.5T120-384q0 50 35 85t85 35Zm240-216Z"/></svg> \ No newline at end of file diff --git a/src/assets/images/ico-model-documentation-green.png b/src/assets/images/ico-model-documentation-green.png new file mode 100644 index 0000000000000000000000000000000000000000..fe065c5fb9abb1324cf357e2a686c2f83651c5a0 Binary files /dev/null and b/src/assets/images/ico-model-documentation-green.png differ diff --git a/src/assets/images/ico_model_documentation_grey.png b/src/assets/images/ico_model_documentation_grey.png new file mode 100644 index 0000000000000000000000000000000000000000..85a86247694c57951918ccf42dd82269b96f88c9 Binary files /dev/null and b/src/assets/images/ico_model_documentation_grey.png differ diff --git a/src/assets/images/icon-yet-not-publish.png b/src/assets/images/icon-yet-not-publish.png new file mode 100644 index 0000000000000000000000000000000000000000..b47a8a98b4678091027b050bbcd55603ff996363 Binary files /dev/null and b/src/assets/images/icon-yet-not-publish.png differ diff --git a/src/assets/images/request_approval.png b/src/assets/images/request_approval.png new file mode 100644 index 0000000000000000000000000000000000000000..6b43076a6bab5c1891f5471b83c2f2a12332d505 Binary files /dev/null and b/src/assets/images/request_approval.png differ diff --git a/src/assets/images/request_approval_selected.png b/src/assets/images/request_approval_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..540b6c7f2ba5de9e3139be9e862ca8642d40f0ca Binary files /dev/null and b/src/assets/images/request_approval_selected.png differ diff --git a/src/assets/images/stepper_progress.png b/src/assets/images/stepper_progress.png new file mode 100644 index 0000000000000000000000000000000000000000..bf0d9b53f884af86ce16cac7a28fe0b45b37daf0 Binary files /dev/null and b/src/assets/images/stepper_progress.png differ