diff --git a/src/app/core/services/public-solutions.service.ts b/src/app/core/services/public-solutions.service.ts index 98a265ceba1e53d4b919b514955b1ae433465f2a..c138d74ad634315239d56103fb1b79d81ec95228 100644 --- a/src/app/core/services/public-solutions.service.ts +++ b/src/app/core/services/public-solutions.service.ts @@ -9,6 +9,7 @@ import { AuthorPublisherModel, AverageRatings, Catalog, + DocumentModel, LicenseProfileModel, PublicSolution, PublicSolutionDetailsModel, @@ -233,7 +234,7 @@ export class PublicSolutionsService { solutionId: string, revisionId: string, selectedCatalogId: string, - ) { + ): Observable<DocumentModel[]> { const url = apiConfig.apiBackendURL + apiConfig.urlGetSolutionDocuments( 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 bb0a9bf3716a69a76de0d4cb79a2a3df7ba8d978..55d178e1b0100c820396ff8cc5df462374ff6ba3 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 @@ -101,39 +101,40 @@ } <!--license profile end--> <!--upload documents start--> - <div class="upload-file-containe"> - <span class="upload-file-text">Upload image model </span> - - <mat-form-field class="mat-form-field-upload-file"> - <input matInput [value]="imageFile?.name" /> + <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> + </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 - #fileDropRef - type="file" - id="fileInput" - name="fileInput" - (change)="selectImageFile($event)" - hidden - formControlName="image" + matInput + [matChipInputFor]="documentsGrid" + (matChipInputTokenEnd)="onAddEditDocuments($event)" /> </mat-form-field> - - <button - class="upload-file-button" - color="primary" - mat-raised-button - (click)="onClickUploadImageFile()" - > - Upload file - </button> </div> <!--upload documents end--> <!--tags starts--> - <div class="tags-container"> - <mat-form-field class="flex-grow"> - <mat-label>Add tags</mat-label> - <mat-chip-grid #chipGrid formControlName="tags"> - @for (tag of tags(); track tag) { + <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"> @@ -143,16 +144,29 @@ } </mat-chip-grid> <input - #userInput + #tagInput + [formControl]="tagCtrl" [matChipInputFor]="chipGrid" - (matChipInputTokenEnd)="addTag($event)" + [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.tag + }}</span></mat-option + > + } + </mat-autocomplete> </mat-form-field> - <button mat-raised-button color="primary">Add tags</button> + <button mat-raised-button color="primary">Add tag</button> </div> <!--tags ends--> <!--upload image start--> - <div class="upload-file-container"> + <div class="flex-row"> <span class="upload-file-text">Upload image model </span> <mat-form-field class="mat-form-field-upload-file"> @@ -168,14 +182,12 @@ formControlName="image" /> </mat-form-field> - - <button - class="upload-file-button" - color="primary" - mat-raised-button - (click)="onClickUploadImageFile()" - > - Upload file + <button class="custom-icon" (click)="onClickUploadImageFile()"> + @if (publishToMarketPlaceForm.value.image) { + <mat-icon>edit</mat-icon> + } @else { + <mat-icon>add</mat-icon> + } </button> </div> <!--upload image end--> 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 5a9094b4227444f5708850881425af97f7f8ca68..ef413c75960fad7ff4808105da65d4b0ad17857a 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 @@ -42,10 +42,8 @@ .upload-file-container { display: flex; - flex-direction: row; - width: 100%; + flex-direction: column; gap: 20px; - align-items: center; } .upload-file-text { 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 9dba9f7b8e213d4d43b829f2b45769379fca0100..56dd7ca8d202521e43ddff2f62c5912547e46440 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 @@ -1,4 +1,11 @@ -import { Component, inject, OnInit, Signal, signal } from '@angular/core'; +import { + Component, + ElementRef, + inject, + OnInit, + signal, + ViewChild, +} from '@angular/core'; import { CommonModule } from '@angular/common'; import { ActivatedRoute } from '@angular/router'; import { PublicSolutionsService } from 'src/app/core/services/public-solutions.service'; @@ -7,6 +14,7 @@ import { AlertType, Catalog, CatalogFilter, + DocumentModel, Filter, LicenseProfileModel, PublicSolutionDetailsModel, @@ -21,13 +29,14 @@ import { MatButtonModule } from '@angular/material/button'; import { PrivateCatalogsService } from 'src/app/core/services/private-catalogs.service'; import { FormBuilder, + FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators, } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatSelectModule } from '@angular/material/select'; +import { MatSelectChange, MatSelectModule } from '@angular/material/select'; import { MatInputModule } from '@angular/material/input'; import { FiltersService } from 'src/app/core/services/filters.service'; import { QuillModule } from 'ngx-quill'; @@ -43,14 +52,20 @@ import { RichTextEditorDialogComponent } from '../rich-text-editor-dialog/rich-t import { catchError, combineLatest, + forkJoin, map, Observable, of, + startWith, switchMap, tap, } from 'rxjs'; import { UpdateModelNameDialogComponent } from '../update-model-name-dialog/update-model-name-dialog.component'; import { UpdateSolutionRequestPayload } from '../../models/request-payloads'; +import { + MatAutocompleteModule, + MatAutocompleteSelectedEvent, +} from '@angular/material/autocomplete'; interface RouteParams { solutionId: string; revisionId: string; @@ -59,10 +74,10 @@ interface RouteParams { interface ModelData { solutionId: string; revisionId: string; - solution?: PublicSolutionDetailsModel | null; // Allow null here if your data source might not provide a solution. - picture?: any | null; // Adjust type as needed - description?: any | null; // Use string or any, and allow for null if your fetch might fail - licenseProfile?: LicenseProfileModel | LicenseProfileModel[] | null; // Allow both array and null + solution?: PublicSolutionDetailsModel | null; + picture?: any | null; + description?: any | null; + licenseProfile?: LicenseProfileModel | LicenseProfileModel[] | null; category?: CatalogFilter | null; toolkitType?: CatalogFilter | null; image?: any | null; @@ -87,6 +102,7 @@ interface ModelData { ModelDetailsLicenseProfileComponent, MatChipsModule, MatIconModule, + MatAutocompleteModule, ], templateUrl: './publish-to-marketplace-page.component.html', styleUrl: './publish-to-marketplace-page.component.scss', @@ -102,11 +118,16 @@ export class PublishToMarketplacePageComponent implements OnInit { solution!: PublicSolutionDetailsModel; categories: CatalogFilter[] = []; toolkitTypes: CatalogFilter[] = []; + tagCtrl = new FormControl(''); + @ViewChild('tagInput') tagInput!: ElementRef<HTMLInputElement>; + filteredTags: Observable<Tag[]>; imageFile!: { content: File; name: string }; documentFile!: { content: File; name: string }; licenseProfile!: { content: File; name: string }; - readonly tags = signal<Tag[]>([]); + tagsItems = signal<Tag[]>([]); + documents = signal<DocumentModel[]>([]); + descInnerHTML!: string; blured = false; @@ -178,15 +199,36 @@ export class PublishToMarketplacePageComponent implements OnInit { }), catchError((error) => { console.error('Error in combined data stream:', error); - return of({} as ModelData); // Return an empty ModelData object on error + return of({} as ModelData); }), ); }), catchError((error) => { console.error('Error fetching parameters:', error); - return of({} as ModelData); // Provide a fallback empty ModelData object + return of({} as ModelData); }), - ) || of({} as ModelData); // + ) || of({} as ModelData); + + this.filteredTags = this.tagCtrl.valueChanges.pipe( + startWith(''), + map((value) => { + return this.transformValue(value); + }), + map((name) => (name ? this._filterTag(name) : this.tagsItems().slice())), + ); + } + + transformValue(value: string | Tag | null): string { + if (typeof value === 'string') { + return value; + } else if (value) { + return this.displayFn(value); + } + return ''; + } + + displayFn(tag: Tag): string { + return tag ? `${tag.tag}`.trim() : ''; } buildForm() { @@ -204,8 +246,8 @@ export class PublishToMarketplacePageComponent implements OnInit { category: [null, [Validators.required]], toolkitType: [null, Validators.required], licenseProfile: [null, [Validators.required]], - documents: [null, [Validators.required]], - tags: [null, [Validators.required]], + documents: [[], [Validators.required]], + tags: [[], [Validators.required]], image: [null, [Validators.required]], }); } @@ -260,36 +302,54 @@ export class PublishToMarketplacePageComponent implements OnInit { ); } - onChangeCatalog(event: any) { + onChangeCatalog(event: MatSelectChange) { this.selectedCatalog = event.value; - this.publicSolutionsService - .getSolutionDescription(this.revisionId, this.selectedCatalog.catalogId) - .subscribe({ - next: (res) => { - this.publishToMarketPlaceForm.patchValue({ - description: res.response_body.description, - }); - error: (error: any) => {}; - }, - }); + + if (this.selectedCatalog.catalogId !== '') { + this.publicSolutionsService + .getSolutionDescription(this.revisionId, this.selectedCatalog.catalogId) + .subscribe({ + next: (res) => { + this.publishToMarketPlaceForm.patchValue({ + description: res.response_body.description, + }); + }, + error: (error) => { + console.error('Failed to fetch description', error); + // Handle the error, e.g., set a default value or show an error message + }, + }); + + 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); + }, + }); + } } submit() {} created(event: any) {} - focus($event: any) { - this.focused = true; - this.blured = false; - } - - blur($event: any) { - this.focused = false; - this.blured = true; - } - onaAddEditImage() {} - onAddEditDocuments() {} + onAddEditDocuments(event: MatChipInputEvent) {} onAddEditLicenseProfile() {} getFilenameExtension(filename: string): string { @@ -327,7 +387,7 @@ export class PublishToMarketplacePageComponent implements OnInit { } } - async onClickUploadImageFile() { + onClickUploadImageFile() { const dialogRef: MatDialogRef<UploadLicenseProfileComponent> = this.dialog.open(UploadLicenseProfileComponent, { data: { @@ -368,7 +428,7 @@ export class PublishToMarketplacePageComponent implements OnInit { } remove(tag: Tag) { - this.tags.update((tags: Tag[]) => { + this.tagsItems.update((tags: Tag[]) => { const index = tags.indexOf(tag); if (index < 0) { return tags; @@ -380,12 +440,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]; + }); + } + addTag(event: MatChipInputEvent): void { const value = { tag: (event.value || '').trim() }; // Add our keyword if (value) { - this.tags.update((tags: Tag[]) => [...tags, value]); + this.tagsItems.update((tags: Tag[]) => [...tags, value]); } // Clear the input value @@ -452,8 +525,9 @@ export class PublishToMarketplacePageComponent implements OnInit { code: data.solution.tookitType, }); - if (data.documents) + if (data.documents) { this.publishToMarketPlaceForm.patchValue({ documents: data.documents }); + } if ( data.solution && @@ -461,7 +535,7 @@ export class PublishToMarketplacePageComponent implements OnInit { data.solution.solutionTagList.length >= 1 ) { const tagsList = data.solution?.solutionTagList; - this.tags.update((tags: Tag[]) => [...tags, ...tagsList]); + this.tagsItems.update((tags: Tag[]) => [...tags, ...tagsList]); this.publishToMarketPlaceForm.patchValue({ tags: data.solution?.solutionTagList, }); @@ -518,4 +592,18 @@ export class PublishToMarketplacePageComponent implements OnInit { compareObjects(o1: CatalogFilter, o2: CatalogFilter): boolean { return o1.name === o2.name && o1.code === o2.code; } + + selected(event: MatAutocompleteSelectedEvent): void { + this.tagsItems.update((tags: Tag[]) => [...tags, event.option.value]); + this.tagInput.nativeElement.value = ''; + this.tagCtrl.setValue(null); + } + + private _filterTag(value: string): Tag[] { + const filterValue = value.toLowerCase(); + + return this.tagsItems().filter((tag) => + tag.tag.toLowerCase().includes(filterValue), + ); + } } diff --git a/src/app/shared/components/share-with-team-page/share-with-team-page.component.ts b/src/app/shared/components/share-with-team-page/share-with-team-page.component.ts index 081cd63ebd8f1ee60e22904a30bb0323a8fe5682..d8552496f003108eb9ad7eba6635b5127366e59d 100644 --- a/src/app/shared/components/share-with-team-page/share-with-team-page.component.ts +++ b/src/app/shared/components/share-with-team-page/share-with-team-page.component.ts @@ -65,14 +65,10 @@ export interface State { }) export class ShareWithTeamPageComponent implements OnInit { separatorKeysCodes: number[] = [ENTER, COMMA]; - fruitCtrl = new FormControl(''); userCtrl = new FormControl(''); - filteredFruits: Observable<string[]>; filteredUsers: Observable<UserDetails[]>; - fruits: string[] = ['']; users: UserDetails[] = []; - allFruits: string[] = ['Apple', 'Lemon', 'Lime', 'Orange', 'Strawberry']; allUsersList: UserDetails[] = []; allUserDetails: UserDetails[] = []; @@ -84,7 +80,6 @@ export class ShareWithTeamPageComponent implements OnInit { selectedRevisionSubscription!: Subscription; solution!: PublicSolutionDetailsModel; - @ViewChild('fruitInput') fruitInput!: ElementRef<HTMLInputElement>; @ViewChild('userInput') userInput!: ElementRef<HTMLInputElement>; announcer = inject(LiveAnnouncer); @@ -97,13 +92,6 @@ export class ShareWithTeamPageComponent implements OnInit { public dialog: MatDialog, private alertService: AlertService, ) { - this.filteredFruits = this.fruitCtrl.valueChanges.pipe( - startWith(null), - map((fruit: string | null) => - fruit ? this._filter(fruit) : this.allFruits.slice(), - ), - ); - this.filteredUsers = this.userCtrl.valueChanges.pipe( startWith(''), // Start with an empty string map((value) => { @@ -160,14 +148,6 @@ export class ShareWithTeamPageComponent implements OnInit { this.userCtrl.setValue(null); } - private _filter(value: string): string[] { - const filterValue = value.toLowerCase(); - - return this.allFruits.filter((fruit) => - fruit.toLowerCase().includes(filterValue), - ); - } - private _filterUser(value: string): UserDetails[] { const filterValue = value.toLowerCase(); diff --git a/src/app/shared/models/document.model.ts b/src/app/shared/models/document.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..4c110c7c0c27adf38ca9c779c07f163c97ace584 --- /dev/null +++ b/src/app/shared/models/document.model.ts @@ -0,0 +1,10 @@ +export interface DocumentModel { + created: string; + modified: string; + documentId: string; + name: string; + version: string; + uri: string; + size: number; + userId: string; +} diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index 2c969b316065faa7c69ffa65a7ff1546daa3c7d0..081bcc5a4f45ca2eb51eddc18ec8ba13da2b3fb4 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -5,3 +5,4 @@ export * from './public-solution.model'; export * from './comment'; export * from './navigation-label.model'; export * from './empty-models.model'; +export * from './document.model';