diff --git a/src/app/core/services/private-catalogs.service.ts b/src/app/core/services/private-catalogs.service.ts index ff7ff41e189c9d97fed1a79175f3f1b1350134b4..d8656f65f166ee71292678b2c6de509b3ddf4b39 100644 --- a/src/app/core/services/private-catalogs.service.ts +++ b/src/app/core/services/private-catalogs.service.ts @@ -7,6 +7,7 @@ import { AuthorPublisherModel, Catalog, CommentModel, + DocumentModel, PublicSolution, PublicSolutionDetailsModel, PublicSolutionsRequestPayload, @@ -784,8 +785,9 @@ export class PrivateCatalogsService { revisionId: string, selectedCatalogId: string, file: File, - ) { + ): Observable<DocumentModel> { const uploadUrl = + apiConfig.apiBackendURL + '/api/solution/' + solutionId + '/revision/' + @@ -796,27 +798,31 @@ export class PrivateCatalogsService { const formData = new FormData(); formData.append('file', file); - const req = new HttpRequest('POST', uploadUrl, formData); - - return this.http.request(req); + return this._httpSharedService.post(uploadUrl, undefined, formData).pipe( + map((res) => res.response_body), + catchError((error) => { + throw error; + }), + ); } getSolutionsFiles() {} - DeleteSolutionsFiles( + deleteSolutionsFiles( solutionId: string, revisionId: string, selectedCatalogId: string, documentId: string, ) { const url = + apiConfig.apiBackendURL + '/api/solution/' + solutionId + '/revision/' + revisionId + '/' + selectedCatalogId + - '/document' + + '/document/' + documentId; return this._httpSharedService.delete(url).pipe( 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 c3d7aa1764a96c31cb2018208385680664e4219a..c5c8c0af2a48f656f3ac901296cac21f9fa84611 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 @@ -49,7 +49,7 @@ ></div> </div> - <div class="select-category"> + <div class="flex-column"> <mat-form-field class="full-width"> <mat-label>Select category</mat-label> <mat-select @@ -104,7 +104,14 @@ <div class="upload-file-container"> <div class="flex-row"> <span class="upload-file-text">Model Documents </span> - <button color="primary" mat-raised-button>upload</button> + + <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> @@ -130,47 +137,66 @@ </div> <!--upload documents end--> <!--tags starts--> - <div class="flex-row"> - <mat-form-field class="full-with"> - <mat-label>Add tag</mat-label> - <mat-chip-grid #chipGrid> - @for (tag of tagsItems(); track tag) { - <mat-chip-row (removed)="remove(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" - /> - <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 - > - } - </mat-autocomplete> - </mat-form-field> - <button mat-raised-button color="primary">Add tag</button> - </div> + <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)="remove(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 + > + } + </mat-autocomplete> + </mat-form-field> <!--tags ends--> <!--upload image start--> - <div class="flex-row"> + <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> + </div> + </div> - <mat-form-field class="mat-form-field-upload-file"> - <input matInput [value]="imageFile?.name" /> + <!-- <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 /> <input #fileDropRef @@ -181,14 +207,14 @@ hidden formControlName="image" /> - </mat-form-field> - <button class="custom-icon" (click)="onClickUploadImageFile()"> - @if (publishToMarketPlaceForm.value.image) { + </mat-form-field> --> + <!-- <button class="custom-icon" (click)="onClickUploadImageFile()"> + @if (imageToShow) { <mat-icon>edit</mat-icon> } @else { <mat-icon>add</mat-icon> } - </button> + </button> --> </div> <!--upload image end--> </div> 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 ef413c75960fad7ff4808105da65d4b0ad17857a..c0a41a5b001e0f5378a3eb429cf83d0159f09e17 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 @@ -79,3 +79,68 @@ width: 20%; } } + +.single-file { + display: flex; + padding: 0.5rem; + align-items: center; + border: 1px dashed #d5d5d5; + gap: 8px; + + .name { + font-size: 14px; + font-weight: 500; + margin: 0; + color: #666; + } + + /* .info { + width: 100%; + } */ +} +.file-image { + align-self: flex-start; + border: 1px solid #dbcbe8; + width: 24px; + height: 24px; + padding: 3px; +} + +.uploaded-img-des { + border: 1px solid #dbcbe8; + padding: 5px; + font-size: 13px; + line-height: 16px; + float: left; +} +.uploaded-img-des > .imagecontainer { + width: 24px; + height: 24px; + margin-right: 10px; + float: left; + border: 1px solid #dbcbe8; + padding: 3px; +} + +.file-name { + gap: 4px; + color: #666; + float: left; + align-items: center; + a { + color: #0366d6; + font-size: 11px; + cursor: pointer; + } +} + +.upload-text-hint { + font-size: 14px; + letter-spacing: 0; + margin: 0 0 16px; + line-height: 24px; + word-break: break-word; + overflow-wrap: break-word; + hyphens: auto; + color: #666; +} 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 37ac5cc72964394137242898f2a4ed51d9795a93..898e24d621efe90734741dc80756137dcecf637c 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 @@ -50,6 +50,7 @@ import { MatIconModule } from '@angular/material/icon'; import { LiveAnnouncer } from '@angular/cdk/a11y'; import { RichTextEditorDialogComponent } from '../rich-text-editor-dialog/rich-text-editor-dialog.component'; import { + BehaviorSubject, catchError, combineLatest, map, @@ -65,6 +66,7 @@ import { MatAutocompleteModule, MatAutocompleteSelectedEvent, } from '@angular/material/autocomplete'; +import { COMMA, ENTER } from '@angular/cdk/keycodes'; interface RouteParams { solutionId: string; revisionId: string; @@ -119,7 +121,7 @@ export class PublishToMarketplacePageComponent implements OnInit { toolkitTypes: CatalogFilter[] = []; tagCtrl = new FormControl(''); @ViewChild('tagInput') tagInput!: ElementRef<HTMLInputElement>; - filteredTags!: Observable<string[]>; + filteredTags = new BehaviorSubject<string[]>([]); imageFile!: { content: File; name: string }; documentFile!: { content: File; name: string }; @@ -140,6 +142,23 @@ export class PublishToMarketplacePageComponent implements OnInit { announcer = inject(LiveAnnouncer); 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); + } + } constructor( private activatedRoute: ActivatedRoute, @@ -271,19 +290,32 @@ export class PublishToMarketplacePageComponent implements OnInit { (err: any) => {}, ); - this.filteredTags = combineLatest([ - this.tagCtrl.valueChanges.pipe(startWith('')), - ]).pipe( - map(([inputValue]) => { - const tags = this.allTags(); - return inputValue ? this.filterTags(inputValue, tags) : tags; - }), + this.tagCtrl.valueChanges + .pipe( + startWith(''), + map((inputValue) => + inputValue + ? this.filterTags(inputValue, this.allTags()) + : this.allTags(), + ), + ) + .subscribe((filteredTags) => { + this.filteredTags.next(filteredTags); + }); + } + + private filterTags(inputValue: string, tags: string[]): string[] { + return tags.filter((tag) => + tag.toLowerCase().includes(inputValue.toLowerCase()), ); } - filterTags(inputValue: string, tags: string[]): string[] { - const filterValue = inputValue.toLowerCase(); - return tags.filter((tag) => tag.toLowerCase().includes(filterValue)); + updateAllTagsSignal(value: string, tags: string[]): string[] { + const updatedTags = tags.filter( + (tag) => tag.toLowerCase() !== value.toLocaleLowerCase(), + ); + this.allTags.update((tags: string[]) => [...updatedTags]); + return updatedTags; } convertToolkitTypesToCodes( @@ -398,6 +430,15 @@ export class PublishToMarketplacePageComponent implements OnInit { dialogRef.afterClosed().subscribe((result) => {}); } + onClickUpdateSolutionDocument(file: File) { + return this.privateCatalogsService.updateSolutionFiles( + this.solutionId, + this.revisionId, + this.selectedCatalog.catalogId, + file, + ); + } + onClickUploadDocumentFile() { const dialogRef: MatDialogRef<UploadLicenseProfileComponent> = this.dialog.open(UploadLicenseProfileComponent, { @@ -406,14 +447,31 @@ export class PublishToMarketplacePageComponent implements OnInit { title: 'Upload document model', isEditMode: false, isCheckExtension: false, - action: (file: File) => {}, + action: (file: File) => this.onClickUpdateSolutionDocument(file), isLicenseProfile: false, isProcessEvent: false, }, }, }); - dialogRef.afterClosed().subscribe((result) => {}); + dialogRef.afterClosed().subscribe((result) => { + this.updateDocuments(); + }); + } + + updateDocuments() { + this.publicSolutionsService + .getSolutionDocuments( + this.solutionId, + this.revisionId, + this.selectedCatalog.catalogId, + ) + .subscribe({ + next: (res) => { + this.documents.update((dcs) => [...res]); + }, + error: (error) => {}, + }); } onLicenseProfileClick() { @@ -434,16 +492,25 @@ export class PublishToMarketplacePageComponent implements OnInit { } removeDocument(document: DocumentModel) { - this.documents.update((documents: DocumentModel[]) => { - const index = documents.indexOf(document); - if (index < 0) { - return documents; - } - - documents.splice(index, 1); - this.announcer.announce(`Removed ${document}`); - return [...documents]; - }); + this.privateCatalogsService + .deleteSolutionsFiles( + this.solutionId, + this.revisionId, + this.selectedCatalog.catalogId, + document.documentId, + ) + .subscribe({ + next: (res) => { + const alert: Alert = { + message: 'Document deleted successfully', + type: AlertType.Success, + }; + this.alertService.notify(alert, 5000); + this.alertService.notify; + this.updateDocuments(); + }, + error: () => {}, + }); } addTag(event: MatChipInputEvent): void { @@ -452,12 +519,20 @@ export class PublishToMarketplacePageComponent implements OnInit { // Add our keyword if (value) { this.tagsItems.update((tags: Tag[]) => [...tags, value]); + this.filteredTags.next( + this.updateAllTagsSignal(event.value, this.allTags()), + ); } // Clear the input value event.chipInput!.clear(); } + onChangeInput(event: any) { + // if() + //event.target.value.trim().toLowerCase() + } + extractDescription(html: string): string { return html.replace(/<[^>]+>/gm, ''); } @@ -503,8 +578,7 @@ export class PublishToMarketplacePageComponent implements OnInit { this.revisionId = data.revisionId; this.solutionId = data.solutionId; - if (data.picture) - this.publishToMarketPlaceForm.patchValue({ image: 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, @@ -591,8 +665,14 @@ export class PublishToMarketplacePageComponent implements OnInit { } selected(event: MatAutocompleteSelectedEvent): void { - this.tagsItems.update((tags: Tag[]) => [...tags, event.option.value]); - this.tagInput.nativeElement.value = ''; + this.tagsItems.update((tags: Tag[]) => [ + ...tags, + { tag: event.option.value }, + ]); + event.option.deselect(); this.tagCtrl.setValue(null); + this.filteredTags.next( + this.updateAllTagsSignal(event.option.value, this.allTags()), + ); } } 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 b887c62363bb66f5a15d23dff9340ef5bc5a1f9b..8a2c81ab3871554cd9942e69e831a31988156353 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 @@ -132,12 +132,17 @@ export class UploadLicenseProfileComponent implements OnInit, OnDestroy { if (fileExtension !== this.expectedExtension) { this.handleUploadError(this.errorMessage); - return; // Stop further execution + return; } } + this.data.dataKey.action(file).subscribe({ next: (event: any) => { - this.processEvent(event); + console.log({ event }); + if (this.isProcessEvent) this.processEvent(event); + else { + this.handleUploadSuccess(event); + } }, error: (err: any) => { this.handleUploadError(err);