From 9c451bed6690f7c13b0942feb8d7527ef61a7f74 Mon Sep 17 00:00:00 2001 From: kaw67872 <kawtar.laariche@iais.fraunhofer.de> Date: Tue, 23 Jul 2024 11:21:53 +0200 Subject: [PATCH] #24: add stepper feature --- src/app/core/config/api-config.ts | 3 + .../core/http-shared/http-shared.service.ts | 17 +- .../core/services/private-catalogs.service.ts | 65 ++- ...model-details-license-profile.component.ts | 36 +- ...publish-to-marketplace-page.component.html | 549 ++++++++++++------ ...publish-to-marketplace-page.component.scss | 209 ++++++- .../publish-to-marketplace-page.component.ts | 359 +++++++++++- .../upload-license-profile.component.html | 1 + .../upload-license-profile.component.ts | 2 + src/app/shared/models/index.ts | 1 + .../shared/models/publish-request.model.ts | 21 + src/assets/images/cloud-active.svg | 1 + src/assets/images/cloud.svg | 1 + .../images/ico-model-documentation-green.png | Bin 0 -> 1770 bytes .../images/ico_model_documentation_grey.png | Bin 0 -> 1333 bytes src/assets/images/icon-yet-not-publish.png | Bin 0 -> 1455 bytes src/assets/images/request_approval.png | Bin 0 -> 1007 bytes .../images/request_approval_selected.png | Bin 0 -> 922 bytes src/assets/images/stepper_progress.png | Bin 0 -> 833 bytes 19 files changed, 1039 insertions(+), 226 deletions(-) create mode 100644 src/app/shared/models/publish-request.model.ts create mode 100644 src/assets/images/cloud-active.svg create mode 100644 src/assets/images/cloud.svg create mode 100644 src/assets/images/ico-model-documentation-green.png create mode 100644 src/assets/images/ico_model_documentation_grey.png create mode 100644 src/assets/images/icon-yet-not-publish.png create mode 100644 src/assets/images/request_approval.png create mode 100644 src/assets/images/request_approval_selected.png create mode 100644 src/assets/images/stepper_progress.png diff --git a/src/app/core/config/api-config.ts b/src/app/core/config/api-config.ts index ef0c8f3..025faa8 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 cf200e6..fc71fad 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 3c2d9c2..cc7776a 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 7418035..c142aca 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 8fd346a..9792b0a 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 c0a41a5..387aef2 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 c4671ce..7bf2259 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 694b556..0a8cc52 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 8a2c81a..d24978b 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 081bcc5..e3fdb9f 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 0000000..4123ab9 --- /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 0000000..638ee54 --- /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 0000000..384744e --- /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 GIT binary patch literal 1770 zcmbVNX;2eq7>=bC#F198NSRTWKm(;FyBk7aBgc?Sq9mn=5<JjNc7b4$jmbh1EIOi6 zPL0Jv%ONUPt$=t9^%$sFH5e77PysI#JUXTIq#{Myje_kD#~<C9-TmHg-{*Os?>%O7 zqGbzbID0zNXtWuUNMQ{1o=4puIZdUWZO?{%>;@^4lS_z1(unGDS_DQYa6qC#lkgZE z#mwv4@DLi!L92?DlX7VkPf2K4sNIKU(OjTRDa(xJ+=I0-25B$YOl{@_Fl9Z+GR z^teDNB-IJ<WL0Fk9$%6!i&dttRU#NYJPZgi@hAcfPNINGqt+UDrcnAzULLi!Z-aE; zr3<+>l>SPTTpA4s2t5u2vbano91siyAuKo$31)NV18fN9f)E1293}+wAcP0Oz{EwT zqUo_jUW`yY5sR`y>B%Ii<AI>jXk-}!ScE<agb@S*AvVZnGbs<I!K@`w6H{yOo@5Z> z2Blu5BUOYJurs0xB9#oKQ%YYQL8FsOUlVH$6N#cy2AWVE2(utiqp_dsrMH2M!T*)< zMsGu`S%-r$xPeI3E2(-UdQXC>+`T=J-H}qm`%JH*ih`<zgfdlwYe|VPlumtNVJeI# zhQtwY03TuVg9AiCFdWQh3&kP?4nkl)M}R<+9B;xRe13qC%@&J9LM{x8f+7S#91#WP zBEg7&&E-yFC0YZCYL)n8Ton~}0xNnemM75TC`ssJ2|_*D0ny0>Nf?p|9Uu_cD;@;+ z#i_IyVKjVVFVU+L3vs<F4adZKLIb?aF;Ddd4N4TnQ8WNza!?M!gcTf^i9(!2CL7|i zVJ?O#*@?mQ2|V_HatBg^Kzoz?r)eg)s7|ymUylG~ydEN4OHGcR8j)R<W+!U$TqVMY zSW{nJ(apt$pL?9ExKg9YyV$s;clm7AwN*#-X@{5bS8)BOel0=CB~Kl@3hG4fZXR>I zkmtwoJ7KYA4KZvivWI=rda6t=Xj2^7v-yQ}IE1*@bG78j$p*);fsFP`hUN!PGTP1U zJNl<Y-?z-Ia_h~D-DIlsT5a(focq<@v#m#4YjS+{F1$f+vSj#geeSW-SXfXAj<!rQ zZ}PDKx0f<HFHXVS4|;cJS7oLYJ<BODtngCv6X;2na_h7~*5m9v<DqTE7Gq=re#cy7 z{Puc9oIEUF?py4;=hixhAKqa_-ut<0!`58JqtLnm$M5Ewns+Q6%bmW!K^BR*evt2Y z%I)dH@w6F-WxKpT%(XqQnZwA5JGnC<vwO^c@E84W<u%RHN4C>TAI{5EGon4R%B%i5 zp|Dxr<J-JE5agZI5?)95eV}7zziPmh6_@}U`#l7lv(cvfT$hdSgK}g1rWNJq&+m5` z<A(QmYIb)g@2NjnP||8uNpns`_S7C4@rYlwNbX+o=joxX@sK)5ho(Qut5&yoAHT5N z*tAGKEVxt0a%b${T${P_n~?A=U(}Qq_|HDNs^S9U`l#oNaeb@ve!{P?NxeYY5Vn5a zZpg`bz3EJ2Slf8H4tJ=}viM@I2alIMT&B5lE^eS)Tz@$_r!nQ~v2MSWohi$0m2)cj zt>3G+C*<=FH#~nKURDTYMD>;S{hH>9tdPm;f9KA;W!>&{q^qgyYLvVut@yHNso-wX zRK-4zy1`}KS!vskw^buM+CIe=Y>2$SrE{SCab`7D%hZ-XvidYf7wz}HTYI<YSXbG@ z^ZQ(TIncI__Mfgyi{Gi}ANg^0Yj)|h>0f4PwvH9X`*}s7MZT;2on1CNB)SIlA0EYQ z#~Qo9GhNz?KFwB_S&qGrN1n(=KOUzIkh0PTCE%snfsWLn;_5bQ-|ZBajqN?dZixqC z$7n;PA=&dg+;gwhGH3pMB6m=J16XWk`ODn4$x4ZyTL(HvvV$#E$#=RQ`E}a-m#i=I jZw$O{J1fa$jJvL(4Vt1DH{rB?``<?*k_j95Ycl@<BlW^) literal 0 HcmV?d00001 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 GIT binary patch literal 1333 zcmeAS@N?(olHy`uVBq!ia0vp^;y^6S!3HF&cTd~Kz`*F884^(v;p=0SoS&<gn3A8A zs#lR)0F-B7u(7WwNKDR7Em25HP0!4;ReHaBzmh^`img((sjq==fpcm`rbks#YH*cb zNODznvSo^ry&acLg%!|%+|-gpg^Jvqyke^gTP3i$RzNmLSYJs2tfVB{R>=`$p+baj zfP!;=QL2Keo|$g4p|OR6xuu?=silRHiH?GifuWhcfu+8oiLQa6m4T&|fuRBvDA{o- z*c7FtSp~VcK`jIFY?U%fN(!v>^~=l4^~#O)@{7{-4J|D#^$m>ljf`}GDs+o0^GXsc zbn}XpK}JB#a7isrF3Kz@$;{7F0GXMXlwVq6tE8k4vP2(h3($M|aQ^{0@DNJP0|rYG z(Ekda>#P_Un3sCGIEGmCPQB{uqns*p{NsD$xywsml)Q9m|2~6NXknv3rFQIt1#X)< z4QEwc%6N48<mosAwNr`t`&6oeAHO;}-SpkT6#d<9doz6Q>~kpROgWnHp(Q}aq+#J( zpW81VH}*L)b)A1&Kl8cq`TyqUKlkh?{hY?}L!x5V=Kd|Ocj^E9`){gydfJ6j*$09N z96vZuFuXV*UHT(ep^$9{^A4^%0!}>4st+U|NIhWw#}dbCcd_yG0r3N`|0h3P>b3OK z%@;RrRK{M9Zqzq7H~*QM+IsNd!Q!>Kx#5j&YyW-NySc-OL+1Fx5UsPj*L5dKY(5a~ zr8#Z-v=dK@?(8gPKVHc4VM*Nl=l0z1Ech;Fh`jusee<mEp@$!|f7Z<VbB=HC-n}~d z`pJ=EcJuicym>gas#dat`5fo!Z{l37Yf>YN1y66<wR`u*+}URiF|o6=^YHK(SXz1> z*>s`wuk?ei_SIFqOy3!g72YY0J)4zzv2~d*)B9CXy(vnL3j+?6#+H?rzwCEo+{YTn zP$Z}K`m5Ck*#J8>?c;@9e=olHd-m+v`M0*+o$jM@<Nkg21$+0-op>>@LG=NrJ}3Y8 zq#4Y5j9V9X{;09Ly=l#}bLY<Wc`ZFuW94f%-<`uzMM$ykqf*c*)dwGy1^@T5zxwL? zfSEtsYh}^SnCgOx9jliwf3Dnc<n8U8{CwpW1sOhLGrPViMY0uQKmKMMmwR{AAiTah zlEZOhL`n0qnOk*M^S|&|9+a{<(nK-Cz*p^2Vbj3`u?n6aKYx^8>c9H>K!3xA+~}|0 zze|hn+I^q9)#<s>OrF=TUuVo}OO){O^-Y}hjO~NS@;So%Uya_e9%~UO+Ztv2;?*l7 z=Y<pUKfa5ti;i~o@>CE|5bIB#rRKF#+o|K{<Ohp)Y!{7g6?og~)HrS0v<Z@2v72&d zJ18^=F!^5Y@b$TH<%)~d)I$b6TR;2yF25;rzw3R7mZ+4pv}FHLxd}mf%Y#=s6ssJ% za6w>MW>Vxezl{-pYLb(a!!%Q`WNp3trasSgDW7!5(Il4UgYB!XW-%ygO!eyF;cphW z8nyOYQd;6ecHhfsOQ+n*G-zOxVK6SJFl5xx(|dH|n`!Lz$(Ju*{&wQTiS-<X%F|D~ zKEM1j8VIH@ox)K6M|vIeddBz6_nE&hneyaw<)o8K?q0hVCg$`*bjQ@9%KXyOufWu* zKV#X?IYF<o|2;OlYH4Fq_dxeS;!=@=2^042wJq8y^Yz@X7J-XxZEUYT)}DCCzp%!r b+TtJ66OEw2DJ|k0pwiCM)z4*}Q$iB}*F!~w literal 0 HcmV?d00001 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 GIT binary patch literal 1455 zcmbVMZA{c=9Iq1<QKK>@zGc>;3<!69x!!TDo^p4+!y6vTLF6`>u6OMn9ke~$;#FkH z<{M}*F@(+On3Fk~iW4Sm3C^h-Bm^A`37IneFq~T)$&`sRMwiV!hn(97<A*hApXX`+ zzu*7&f9vkG1$i@)<|jcAG{dus@qu+NxRVnngQxws7Y~7Dp62pvMRG`svI-B`IXS?? z9+3_6KAz>aZ0hBgL(oJ?DE4c9?`k?Ii$*qPW7Ne6Kts^-m3o8?mh&1M;KPDsLq@(l zkH7+FLrTnE%o}m?Wx}c|g)gcqC=OPY2PqC&xdL9U(||zaH5S&z3Q47P8#2aAgMI8Z ziojzgTDc8*EXwa)3p-_nhs{QdA&8r-a1Ld}&6JfOvta_mEht8zIBCFe8lz|ohsQ4j zcvH9#?PJ{IzQD<blxbRoM$u?AYK)qUvJysdilR`AKncPCEDY)vNn>?`q^89g7+wu3 zLPQf}363$c0l8AMAwcQl5X6Yr`-E6h#}frohU#nt#f=y$im|xHtX0j&|10CE)@t#V z2#@-BRjyQmpdO*LI2h#a*^Xj{Kn=Y{5kOJc3PuiAioB$G7#jk<7&(EX-I&{sn;aD3 zu$o*sIBs<ijN3)wITY?7ofH=5cpBDhBJBijVO%Z-AYC~QlZmj~-K5K8vSK7=iDNyI zs<Bd#kNXvX-*K$-nONGX@T?{)#j;!x?|`*svL>r#as+ldV-?SVGfD)BlcQ>8tVE9^ zW_U%|%yVu<7U8iR)523U1X-46SrcX;S&}l~0TMT`7#T7Um<5EzaX}(vMaJ>m|H&N+ z1fj7e`A^ftUx7}HZJ&$)I6N65UILS&fDs{mn>`RT;fjZ`7wcC~Hmu2D3R7yrbEh@* zChbmcYB*h({zG4e*Ea_Vg=_q6cjUL1xb~VC1kx<obroeH{OHMsy@ACSzsYDKt3UZ} zK>YUod0od_|GfR?{X;)4yLl+ucs(-6`d_Nsz9?|!GSoG+Gpn}Cu<+}HJvTbFkFoH! z3-dZ0Db>GE-`dZs8@lJPlbiI@GpMQRd0O5zE5$xDJ9kQhJ+q^wt?|Ph_g1IPQd{nZ z((2#qd$3{9*8~+-H1B~Ir4OYX)cW>xi)n2WiyoexwVP3HC7iha+`ETokJj0SnRP?6 zvkoskkh^%H<I{C{x$0}vf0-H`b?)dmYij(JWKzQYV$yW`2Xty)ea}{X+p@2=mmORs z)sgux+(wc+5+2?=`t#`G-s}F!NV~gx;MH?owSSz^k6rQhWd5C+zl5u;E=d)tg8IO* zo13yPsaX2#YMN;G?;1|N*8iC&;gt4MpL<8EH$Cu9d1=BImBh@YkCqU}ZgeIlnD@MF zBM;xYaA0rZk&V}C22XsS^~O-{CG-0Bs~1}uGE3D5M?z@*A}QfvamL6@1hqceH);Q! zQP=vBkD7b_n$|O^t#!D5b;m1csb$KF&zlG4Z_Mg?zVrU^yZq&I`H%A7ngH!vxL@yR S%v%uqg?d~C%qhp~+x`KE@dsD{ literal 0 HcmV?d00001 diff --git a/src/assets/images/request_approval.png b/src/assets/images/request_approval.png new file mode 100644 index 0000000000000000000000000000000000000000..6b43076a6bab5c1891f5471b83c2f2a12332d505 GIT binary patch literal 1007 zcmV<L0}%X)P)<h;3K|Lk000e1NJLTq000~S000{Z1^@s6ZwT)!00009a7bBm000XT z000XT0n*)m`~Uz0p-DtRR7l6ImS1QbRTRd5yV(W1*|3`;rm?o&*4D9wPPd>8(b8S9 zV8}~0Ew~^>qEF(3tS#6SC2gQSScC@AKJ-BntfDRG7PJktw3Aqk^VG@s(lIqIC?*wc zHg;kRM%RblS+0}Wzj$C^X6~K&?m6c>-#LL&sl@+bimJueKiGLF91i!btgN&J0^A9Q z!yjF`aPDn+U!Wd9Mb+X!5=eWNbimy4=p#F4=YARM>+Om3JhP9^bsbFKnqlIrNj|@H z`Pci`w(tM!+y`IQ1XNTl4dj5{N_!%aHEi7Q(BCJH9c}4c*Rg7S`qm7G2L}KA?ausm z+cFBl3P1@()pX#R@M;w312)^15wI;I0JH;r@mTElfy1x(f=yhX#BmlGdgI7_TOZzh z79i*YmIO>71q=ZR+cNrX%gEc7QE>O!mQe)EjT_d7wr$<A3hd(86^;&`;I8AaJGqnP z<>g%fAs;YZJnptF!>p0s#E$Js#1HJvlc#w7wO84@rwf2C?eVChYUz+1MhPrk2iSk? zhWRab9S6r*EQ9rQ?<=oIqpf&7906i=dgfa3rak=^5dQA_8QyvC48STdfGbxgmH;y( zu`vY<HR73V8Rlb8KK;k+-0fKQz&#>&c05oHqwIaD^v_M(GKwx~a=-#GT%#T{imK`2 z$^fiwj~_gAWN^uG7ResyT?IBfcbk)^-d+0fr=MRe*P)_nC1BLHjQ%QM3E&6M>!)nX z(4R<mzZ{9IdF%P-veCVJx`;+wahydau1|9M%=@&pMW4O=#i!>1LL!_1`5F=QJ4_nT zzt+c2E2?(!?8oN^MnC+d+g)sFY58WpxR9MMF0{JuLPD|-wO7sj*5$e^05ADOCJQ9o z=O7T5KBz{Jyigq1#%;?OX;kEcjcgV<ZWj`lZl<e*fTBx^YF-KzO1lwJR1F~Cs6d3I z+$80kcv@3l)T!5wDXKOO<otjoLKftZx*IbU4mDB%%(YaN`prmv^PY#cWfT=vy8vVq zRf~%*;AP@?6AX3%r{q+*S~@J6Yz{c?Axf^vks}9e^8T;f@kPT;H7MTIRLKMiwq+zM zl|{eMQ(b(}^e`nz7V}f5phqb(VxuX_pc&6}U>ER$U!;PbeCc9#0l1;4+HrB>dUIw< zV7@BkJGF_Ey5+DS?(={tvslzwMqHbKDWJa^2WpLp8Nu9C4F{)y;d->SkwG{ubr!k) d%v!=P+rMJagI+W5D1-n2002ovPDHLkV1f{Y<y8Ox literal 0 HcmV?d00001 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 GIT binary patch literal 922 zcmV;L17-Y)P)<h;3K|Lk000e1NJLTq000{R000{Z1^@s6jnwp200009a7bBm000XT z000XT0n*)m`~Uz0Oi4sRR7l6ImQ83BXBdE=UB}o~Wg}i}$<!5vZ4Qzs0Sg*3rYEte z;16Cj%PKga;({PvjB9%6Nl1&7L1f%kic}1&5>HC9AyE{hZWLXEKZ&j;5z%B#NVaa$ z^zu%a%>K<<-^=dz&3xZ8@BizWnVI2ph_0I2H`TtQ|BO$q23W92c`%qvA2w2<0qY%) z8<?(|wLmT4w`~sq2_qFsoT$Il@zs)w`VD(lDb*X7kr~U<fA<~T(I=VA_|&zQuAMuI z!gSTF1j0ZNSYGHumVUc%x@J?=yhc5c_jz0MJzhT?ZENY;xg#%3SIsaG0enC&5EU)r zc5N3{RvfHay=32>U)On@cVx!0-1_r5je3ByP2HK7_eZL8V7h8XfhK{3?Et4lez@GT zy=Be%uLHgu(7F9L0XX!RMzV95>t~W}N&(RCMk*96<;(2kbX76X{&VXASW&l>>FmFC zUK^;j9FS8wj6OyNC+2{@SUewK_Sb8LEpP<5QWnU=`=P!x0FOF{IKBBs4hSIGIn07Z z$^%QY2#}WOb1h!4r@7~kXCEy<<AFd92%zV})2YE^`m`cB)Bv2aCt!h8Xq8+Wkl<-Q zR(G-g@2jc6+AS*i!oS1lV`AiEw~-3P6=0j}M@uMT`4ss$KcIbhGx`0E-yYw7cIWMi z4SQEsSOGKCm*)DJ$Gq(y8~*wB?nVFw(16}zT8T6{)(rqgDl`gwccT8%k8REOj%O#Q zs{rPIIq%iz;Dj$TKDF37phy{txo}Z0P-9!C?GJzI-qjAYJ4_z|PR{yRs>FZ`SkMMY z%epL4+`ed}LJ6Q!SIt`a&!@oBj%r>g5;ZY7x@v9{YsAE7u3-u5k^-QhM4~TWgzBo< zYNSGu0>?Grh23Xyphj0se}O)Bz5oc>KPj5#vqpp5KdV_xA|@!6#3@}aEL53i&-#Ga zoKCH)X58w;<@lh4j}sDDe!Cg5HOeTTM<UYQ=hRxS(Br@_z*gyL|3|B!t%JmJU;+$_ zf0{lODC5L=sWv%DKW8kkRd!cnf$f}MT+krrw0ypIGw^iT2A+fiph?timF-hSa?aH# w7cP=#R-KFiky4$boFP?n<kP5gyelRD0TuLdbF<&1djJ3c07*qoM6N<$f{atObN~PV literal 0 HcmV?d00001 diff --git a/src/assets/images/stepper_progress.png b/src/assets/images/stepper_progress.png new file mode 100644 index 0000000000000000000000000000000000000000..bf0d9b53f884af86ce16cac7a28fe0b45b37daf0 GIT binary patch literal 833 zcmeAS@N?(olHy`uVBq!ia0vp^-ayR9!3HGLZ$C6;U|{sl42dX-@b$4u&d=3LOvz75 z)vL%Y0Ln8k*w|MTBqnF4mMA2prf25aD!t#mUr8Y|#a1cY)Yrhbz&SM|)1#^=HMq(z zB)KX(*)m1R-j2(r!U||WZfZ%QLPc&)Ua?h$trFN=D<B&rtgoa1R#K8}tK<l>P$9xM zK*2e`C{@8!&rCPj(AYx3+)~fb)Y8JpL`T8Mz|c(Jz*67PMAyL3%D~dfz)%4Sl<c?^ zY>HCStb$zJpq2r7wn`Z#B?VUc`sL;2dgaD?`9<mahL)C=`UXb&Mn<|o6}rWhc_oPz zx_QOQAR{1VxTF>*7iAWdWaj57fXqxx$}cUkRZ`LiS)vcM1?W9}xc`70cnGED0fVIo z=zj&zbymP&-RSA!7!uKXcbcJhvZKK9dd~IM6SMd97sx*la5QjDSia-2jeToVYsaF{ z78AFFhYsA;l$o<PLAk5-%%TZ5UaAYcSsxZOD`HP?&LW%dGo8ch4L+omeY5Y6ym8ZM z@kNCxK`Xx~T+1<Q{=UEEoI=D^qYs=njFsO0`}(MU?YFj#tKu7k6U2YyhI0S4tlO*> z`>A)c@PpsRuZ}GfKE?jNTI|M){D|$5s}C>+u(i#Z+UUGp%qz#L(t_E{ww`mvFHYA> zmsh29gn0P>)J(d3vQ_Zq1hW_G#n(k$eY$QkLpNg-_mbtaTyv~!Bd+Ejh;Vpi|3FHj z>|a}GuH?kGEE2o|72THKBd(r55IE1aHYYMv+KN}+r!D={EUUSUueLVcX4#>z*JiT? zzd(=gb2;Ur%+UQzj~QROCh+}uaWt*xL@3i0=DKxq0V-y5<$6vS7R;IYcpg{%i`*-F z1aCHO>27z>bzbatBUNg)pI*%3R|mF;ualTKKX!R_`dWtI1DY3#%DDa%WG_6jY;wDh z!TX=D-BQl_d+d47oZc)n+fU>A66XaBe;!TT9=GtaWQxZg{WY&x44B^RcF*E3FPbxb U>Z{2QE`n0Nr>mdKI;Vst0ORyU@&Et; literal 0 HcmV?d00001 -- GitLab