Skip to content

Commit

Permalink
feat: add app tab
Browse files Browse the repository at this point in the history
  • Loading branch information
HenryT-CG committed Jan 27, 2024
1 parent 1ef18fb commit df55a88
Show file tree
Hide file tree
Showing 11 changed files with 404 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<div *ngIf="!apps$">
<div class="surface-section my-3">
<p-message severity="error" styleClass="p-2" [text]="'ACTIONS.SEARCH.APP.NOT_FOUND' | translate"></p-message>
</div>
</div>
<ng-container *ngIf="apps$ | async as apps">
<p-dataView
id="product_detail_apps_dataview"
[value]="apps.stream ? apps.stream : []"
[paginator]="true"
[alwaysShowPaginator]="false"
[rowsPerPageOptions]="[10, 20, 50]"
[rows]="viewMode === 'grid' ? 12 : 10"
[layout]="viewMode"
[emptyMessage]="'ACTIONS.SEARCH.NOT_FOUND' | translate"
filterBy="app_name"
[sortField]="sortField"
[sortOrder]="sortOrder"
>
<ng-template pTemplate="header">
<ocx-data-view-controls
[enableFiltering]="true"
[enableSorting]="true"
[supportedViews]="['list']"
[initialViewMode]="viewMode"
[sortingOptions]="[{ label: 'APP.APP_NAME' | translate, value: 'app_name' }]"
[defaultSortOption]="sortField"
[defaultSortDirection]="sortOrder === 1"
(dataViewChange)="onLayoutChange($event)"
(filterChange)="onFilterChange($event)"
[filterColumns]="['APP.APP_NAME' | translate]"
(sortChange)="onSortChange($event)"
(sortDirectionChange)="onSortDirChange($event)"
[translations]="dataViewControlsTranslations"
>
</ocx-data-view-controls
></ng-template>
<ng-template let-app pTemplate="listItem">
<div
class="col-12 grid grid-nogutter align-items-center row-gap-1 listview-row p-1 hover:bg-gray-200 cursor-pointer"
>
<div class="col-12 md:col-5 lg:col-5 xl:col-6">
<div class="flex flex-column justify-content-center gap-1 text-center md:text-left">
<div>{{ limitText(app.app_name, 50) }}</div>
</div>
</div>
</div>
</ng-template>
</p-dataView>
</ng-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import '/src/_mixins.scss';

@include make-disabled-form-readable-input;
@include make-disabled-form-readable-dropdown;
@include prepare-inputgroup;
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { NO_ERRORS_SCHEMA } from '@angular/core'
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'
import { HttpClient } from '@angular/common/http'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { RouterTestingModule } from '@angular/router/testing'
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'
import { of, throwError } from 'rxjs'
import { FormControl, FormGroup, Validators } from '@angular/forms'

import { PortalMessageService } from '@onecx/portal-integration-angular'
import { HttpLoaderFactory } from 'src/app/shared/shared.module'
import { ProductPropertyComponent, ProductDetailForm } from './product-props.component'
import { ProductsAPIService } from 'src/app/generated'

describe('ProductPropertyComponent', () => {
let component: ProductPropertyComponent
let fixture: ComponentFixture<ProductPropertyComponent>

const msgServiceSpy = jasmine.createSpyObj<PortalMessageService>('PortalMessageService', ['success', 'error', 'info'])
const apiServiceSpy = {
createProduct: jasmine.createSpy('createProduct').and.returnValue(of({})),
updateProduct: jasmine.createSpy('updateProduct').and.returnValue(of({}))
}

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ProductPropertyComponent],
imports: [
HttpClientTestingModule,
RouterTestingModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
})
],
schemas: [NO_ERRORS_SCHEMA],
providers: [
{ provide: ProductsAPIService, useValue: apiServiceSpy },
{ provide: PortalMessageService, useValue: msgServiceSpy }
]
}).compileComponents()
}))

beforeEach(() => {
fixture = TestBed.createComponent(ProductPropertyComponent)
component = fixture.componentInstance
fixture.detectChanges()
})

afterEach(() => {
msgServiceSpy.success.calls.reset()
msgServiceSpy.error.calls.reset()
msgServiceSpy.info.calls.reset()
apiServiceSpy.createProduct.calls.reset()
apiServiceSpy.updateProduct.calls.reset()
})

it('should create', () => {
expect(component).toBeTruthy()
})

it('should patchValue in formGroup onChanges if product', () => {
const product = {
id: 'id',
name: 'name',
basePath: 'path'
}
component.product = product
spyOn(component.formGroup, 'patchValue')

component.ngOnChanges()

expect(component.formGroup.patchValue).toHaveBeenCalledWith({ ...product })
expect(component.product.name).toEqual(product.name)
})

it('should reset formGroup onChanges if no product', () => {
spyOn(component.formGroup, 'reset')

component.ngOnChanges()

expect(component.formGroup.reset).toHaveBeenCalled()
})

it('should call createProduct onSubmit in new mode', () => {
apiServiceSpy.createProduct.and.returnValue(of({}))
const formGroup = new FormGroup<ProductDetailForm>({
id: new FormControl<string | null>('id'),
name: new FormControl<string | null>('name'),
operator: new FormControl<boolean | null>(null),
version: new FormControl<string | null>('version'),
description: new FormControl<string | null>(null),
imageUrl: new FormControl<string | null>(null),
basePath: new FormControl<string | null>('path'),
displayName: new FormControl<string | null>('display'),
iconName: new FormControl<string | null>('icon'),
classifications: new FormControl<string[] | null>(null)
})
component.formGroup = formGroup as FormGroup<ProductDetailForm>
component.changeMode = 'CREATE'

component.onSubmit()

expect(apiServiceSpy.createProduct).toHaveBeenCalled()
expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.PRODUCT.OK' })
})

it('should call updateProduct onSubmit in edit mode', () => {
apiServiceSpy.updateProduct.and.returnValue(of({}))
const formGroup = new FormGroup<ProductDetailForm>({
id: new FormControl<string | null>('id'),
name: new FormControl<string | null>('name'),
operator: new FormControl<boolean | null>(null),
version: new FormControl<string | null>('version'),
description: new FormControl<string | null>(null),
imageUrl: new FormControl<string | null>(null),
basePath: new FormControl<string | null>('path'),
displayName: new FormControl<string | null>('display'),
iconName: new FormControl<string | null>('icon'),
classifications: new FormControl<string[] | null>(null)
})
component.formGroup = formGroup as FormGroup<ProductDetailForm>
component.changeMode = 'EDIT'

component.onSubmit()

expect(apiServiceSpy.updateProduct).toHaveBeenCalled()
expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.PRODUCT.OK' })
})

it('should display error if updateProduct fails', () => {
apiServiceSpy.updateProduct.and.returnValue(throwError(() => new Error()))
const formGroup = new FormGroup<ProductDetailForm>({
id: new FormControl<string | null>('id'),
name: new FormControl<string | null>('name'),
operator: new FormControl<boolean | null>(null),
version: new FormControl<string | null>('version'),
description: new FormControl<string | null>(null),
imageUrl: new FormControl<string | null>(null),
basePath: new FormControl<string | null>('path'),
displayName: new FormControl<string | null>('display'),
iconName: new FormControl<string | null>('icon'),
classifications: new FormControl<string[] | null>(null)
})
component.formGroup = formGroup as FormGroup<ProductDetailForm>
component.changeMode = 'EDIT'

component.onSubmit()

expect(component.formGroup.valid).toBeTrue()
expect(msgServiceSpy.error).toHaveBeenCalledWith({
summaryKey: 'ACTIONS.EDIT.PRODUCT.NOK'
})
})

it('should display error if createProduct fails', () => {
apiServiceSpy.createProduct.and.returnValue(throwError(() => new Error()))
const formGroup = new FormGroup<ProductDetailForm>({
id: new FormControl<string | null>('id'),
name: new FormControl<string | null>('name'),
operator: new FormControl<boolean | null>(null),
version: new FormControl<string | null>('version'),
description: new FormControl<string | null>(null),
imageUrl: new FormControl<string | null>(null),
basePath: new FormControl<string | null>('path'),
displayName: new FormControl<string | null>('display'),
iconName: new FormControl<string | null>('icon'),
classifications: new FormControl<string[] | null>(null)
})
component.formGroup = formGroup as FormGroup<ProductDetailForm>
component.changeMode = 'CREATE'

component.onSubmit()

expect(component.formGroup.valid).toBeTrue()
expect(msgServiceSpy.error).toHaveBeenCalledWith({
summaryKey: 'ACTIONS.CREATE.PRODUCT.NOK'
})
})

it('should display error onSubmit if formGroup invalid', () => {
const formGroup = new FormGroup<ProductDetailForm>({
id: new FormControl<string | null>(null, Validators.required),
name: new FormControl<string | null>('name'),
operator: new FormControl<boolean | null>(null),
version: new FormControl<string | null>('version'),
description: new FormControl<string | null>(null),
imageUrl: new FormControl<string | null>(null),
basePath: new FormControl<string | null>('path'),
displayName: new FormControl<string | null>('display'),
iconName: new FormControl<string | null>('icon'),
classifications: new FormControl<string[] | null>(null)
})
component.formGroup = formGroup as FormGroup<ProductDetailForm>

component.onSubmit()

expect(component.formGroup.valid).toBeFalse()
expect(msgServiceSpy.error).toHaveBeenCalledWith({
summaryKey: 'VALIDATION.FORM_INVALID'
})
})

it('should display error onSubmit if formGroup invalid', () => {
const event = {
target: {
files: ['file']
}
}

component.onFileUpload(event as any, 'logo')

expect(component.formGroup.valid).toBeFalse()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Component, Input, OnChanges, ViewChild } from '@angular/core'
import { SelectItem } from 'primeng/api'
import { Observable, finalize } from 'rxjs'

import { DataViewControlTranslations, PortalMessageService } from '@onecx/portal-integration-angular'
import { Product, MicrofrontendsAPIService, MicrofrontendPageResult } from '../../../generated'
import { dropDownSortItemsByLabel, limitText } from 'src/app/shared/utils'
import { IconService } from '../../../shared/iconservice'

@Component({
selector: 'ps-product-apps',
templateUrl: './product-apps.component.html',
styleUrls: ['./product-apps.component.scss']
})
export class ProductAppsComponent implements OnChanges {
@Input() product: Product | undefined
@Input() dateFormat = 'medium'
@Input() changeMode = 'VIEW'
public apps$!: Observable<MicrofrontendPageResult>
public iconItems: SelectItem[] = [{ label: '', value: null }]
public viewMode = 'list'
public filter: string | undefined
public sortField = 'name'
public sortOrder = 1
public searchInProgress = false
public limitText = limitText
public dataViewControlsTranslations: DataViewControlTranslations = {}
@ViewChild(DataView) dv: DataView | undefined

constructor(
private icon: IconService,
private appApi: MicrofrontendsAPIService,
private msgService: PortalMessageService
) {
this.iconItems.push(...this.icon.icons.map((i) => ({ label: i, value: i })))
this.iconItems.sort(dropDownSortItemsByLabel)
}

ngOnChanges(): void {
console.log('apps ngOnChanges')
this.loadApps()
}

public loadApps(): void {
this.searchInProgress = true
this.apps$ = this.appApi
.searchMicrofrontends({
microfrontendSearchCriteria: { productName: this.product?.name, pageSize: 1000 }
})
.pipe(finalize(() => (this.searchInProgress = false)))
}

public onLayoutChange(viewMode: string): void {
this.viewMode = viewMode
}
public onFilterChange(filter: string): void {
this.filter = filter
//this.dv?.filter(filter, 'contains')
}
public onSortChange(field: string): void {
this.sortField = field
}
public onSortDirChange(asc: boolean): void {
this.sortOrder = asc ? -1 : 1
}
}
18 changes: 14 additions & 4 deletions src/app/product-store/product-detail/product-detail.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<p-message severity="error" styleClass="p-2" [text]="'ACTIONS.SEARCH.PRODUCT.NOT_EXISTS' | translate"></p-message>
</div>
<p-tabView *ngIf="(product && !loading) || changeMode === 'CREATE'">
<p-tabPanel id="product_detail_panel_props" [header]="'DIALOG.TABS.PROPERTIES' | translate">
<p-tabPanel id="product_detail_panel_props" class="p-0" [header]="'DIALOG.TABS.PROPERTIES' | translate">
<ps-product-props
[product]="product"
[changeMode]="changeMode"
Expand All @@ -22,12 +22,22 @@
></ps-product-props>
</p-tabPanel>

<p-tabPanel id="product_detail_panel_intern" [header]="'DIALOG.TABS.INTERN' | translate">
<p-tabPanel
id="product_detail_panel_intern"
class="p-0"
[disabled]="changeMode !== 'VIEW'"
[header]="'DIALOG.TABS.INTERN' | translate"
>
<ps-product-intern [product]="product" [dateFormat]="dateFormat"></ps-product-intern>
</p-tabPanel>

<p-tabPanel id="product_detail_panel_apps" [header]="'DIALOG.TABS.APPS' | translate">
<!--p-panel [showHeader]="false" styleClass="pt-1 pb-0 mx-2 my-1 surface-50"> Apps </p-panel-->
<p-tabPanel
id="product_detail_panel_apps"
class="p-0"
[disabled]="changeMode !== 'VIEW'"
[header]="'DIALOG.TABS.APPS' | translate"
>
<ps-product-apps [product]="product" [changeMode]="changeMode"></ps-product-apps>
</p-tabPanel>
</p-tabView>
</ocx-page-content>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
/* limit deletion dialog */
:host ::ng-deep {
// each tab panel manages its own padding separately
.p-tabview .p-tabview-panels {
padding: 0;
padding-left: var(--panel-content-padding);
padding-right: var(--panel-content-padding);
}
// popup dialogues
@media (max-width: 991px) {
.p-dialog.p-dialog {
max-height: unset !important;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="surface-section w-full">
<div class="surface-section w-full my-3 sm:my-5">
<div class="flex flex-wrap flex-column row-gap-4 my-3 w-12 sm:w-10 md:w-8 lg:w-6 xl:w-6">
<div class="flex flex-wrap row-gap-4">
<div class="w-10 sm:w-6 px-3">
Expand Down
Loading

0 comments on commit df55a88

Please sign in to comment.