Skip to content
Snippets Groups Projects
Commit 9c451bed authored by Kawtar Laariche's avatar Kawtar Laariche
Browse files

#24: add stepper feature

parent 9b9831bd
No related branches found
No related tags found
No related merge requests found
Showing
with 1039 additions and 226 deletions
......@@ -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/',
};
......@@ -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;
}
......
......@@ -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;
}),
);
}
}
......@@ -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,
);
}
......
<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>
......
......@@ -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;
}
......@@ -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
},
});
}
}
......@@ -30,6 +30,7 @@
class="upload-txtbox"
id="fileDropRef"
(change)="handleFileInput($event)"
[accept]="accept"
/>
Browse for file
</button>
......
......@@ -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;
......
......@@ -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';
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;
}
<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
<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
src/assets/images/ico-model-documentation-green.png

1.73 KiB

src/assets/images/ico_model_documentation_grey.png

1.3 KiB

src/assets/images/icon-yet-not-publish.png

1.42 KiB

src/assets/images/request_approval.png

1007 B

src/assets/images/request_approval_selected.png

922 B

src/assets/images/stepper_progress.png

833 B

0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment