diff --git a/CHANGELOG.md b/CHANGELOG.md index c7ba8d2d12..7f45d13f4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ _**For better traceability add the corresponding GitHub issue number in each cha - #737 Added concept: Contract table -> parts link action - XXX Added interceptor to EdcRestTemplates to log requests - #915 Added section to documentation: EDC-BPN configuration +- #1037 Added link from contracts view to the corresponding filtered part table view ### Removed diff --git a/frontend/src/app/modules/core/user/table-settings.service.spec.ts b/frontend/src/app/modules/core/user/table-settings.service.spec.ts index 6e2aed7296..bafd29d9a1 100644 --- a/frontend/src/app/modules/core/user/table-settings.service.spec.ts +++ b/frontend/src/app/modules/core/user/table-settings.service.spec.ts @@ -42,12 +42,12 @@ describe('TableSettingsService', () => { it('should return PartsAsPlannedConfigurationModel for AS_PLANNED_OWN', () => { const result: TableViewConfig = service.initializeTableViewSettings(TableType.AS_PLANNED_OWN); - expect(result.displayedColumns.length).toBe(20); + expect(result.displayedColumns.length).toBe(21); }); it('should return PartsAsBuiltConfigurationModel for AS_BUILT_OWN', () => { const result: TableViewConfig = service.initializeTableViewSettings(TableType.AS_BUILT_OWN); - expect(result.displayedColumns.length).toBe(22); + expect(result.displayedColumns.length).toBe(23); }); it('should return NotificationsSentConfigurationModel for SENT_NOTIFICATION', () => { diff --git a/frontend/src/app/modules/page/admin/presentation/contracts/contracts.component.spec.ts b/frontend/src/app/modules/page/admin/presentation/contracts/contracts.component.spec.ts index 141ea7e247..d5c94b99a7 100644 --- a/frontend/src/app/modules/page/admin/presentation/contracts/contracts.component.spec.ts +++ b/frontend/src/app/modules/page/admin/presentation/contracts/contracts.component.spec.ts @@ -1,4 +1,5 @@ import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; import { AdminModule } from '@page/admin/admin.module'; import { AdminFacade } from '@page/admin/core/admin.facade'; import { assembleContract } from '@page/admin/core/admin.model'; @@ -16,16 +17,20 @@ describe('ContractTableComponent', () => { getContracts: jasmine.createSpy().and.returnValue(of(getContracts)), }; + const routerMock = { + navigate: jasmine.createSpy('navigate'), + }; + const renderContractTableComponent = () => renderComponent(ContractsComponent, { imports: [ AdminModule ], - providers: [ { provide: AdminFacade, useValue: mockAdminFacade } ], + providers: [ { provide: AdminFacade, useValue: mockAdminFacade }, { provide: Router, useValue: routerMock } ], }); let createElementSpy: jasmine.Spy; beforeEach(() => { TestBed.configureTestingModule({ declarations: [ ContractsComponent ], - providers: [ AdminFacade, AdminService ], + providers: [ AdminFacade, AdminService, { provide: Router, useValue: routerMock } ], }); createElementSpy = spyOn(document, 'createElement').and.callThrough(); @@ -82,6 +87,24 @@ describe('ContractTableComponent', () => { }); + it('should navigate if viewAssets clicked', async () => { + const { fixture } = await renderContractTableComponent(); + const { componentInstance } = fixture; + componentInstance.viewAssetsClicked.emit({ contractId: 'test' }); + + expect(routerMock.navigate).toHaveBeenCalled(); + }); + + it('should emit viewAssetsClicked', async () => { + const { fixture } = await renderContractTableComponent(); + const { componentInstance } = fixture; + let spy = spyOn(componentInstance.viewAssetsClicked, 'emit'); + const viewAssetsAction = componentInstance.tableConfig.menuActionsConfig.filter(action => action.label === 'actions.viewParts')[0]; + viewAssetsAction.action(null); + expect(spy).toHaveBeenCalled(); + + }); + it('should convert data to csv', async () => { const { fixture } = await renderContractTableComponent(); const { componentInstance } = fixture; diff --git a/frontend/src/app/modules/page/admin/presentation/contracts/contracts.component.ts b/frontend/src/app/modules/page/admin/presentation/contracts/contracts.component.ts index 8726d3840c..8c861c9dff 100644 --- a/frontend/src/app/modules/page/admin/presentation/contracts/contracts.component.ts +++ b/frontend/src/app/modules/page/admin/presentation/contracts/contracts.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, EventEmitter } from '@angular/core'; import { Router } from '@angular/router'; import { Pagination } from '@core/model/pagination.model'; import { AdminFacade } from '@page/admin/core/admin.facade'; @@ -22,6 +22,7 @@ export class ContractsComponent { selectedContracts: Contract[]; contractFilter: any; pagination: TableEventConfig; + viewAssetsClicked: EventEmitter = new EventEmitter; constructor(public readonly adminFacade: AdminFacade, private readonly contractsFacade: ContractsFacade, private readonly router: Router) {} @@ -33,13 +34,26 @@ export class ContractsComponent { } else { this.contractsFacade.setContracts(0,10,[null,null]); } + }) + this.viewAssetsClicked.subscribe((data) => { + this.router.navigate([ 'parts' ], { queryParams: { contractId: data?.['contractId'] } }); + }); + this.pagination = { page: 0, pageSize: 10, sorting: [ '', null ] }; this.tableConfig = { displayedColumns: [ 'select', 'contractId', 'counterpartyAddress', 'creationDate', 'endDate', 'state', 'menu' ], header: CreateHeaderFromColumns([ 'contractId', 'counterpartyAddress', 'creationDate', 'endDate', 'state', 'menu' ], 'pageAdmin.contracts'), - menuActionsConfig: [], + menuActionsConfig: [ + { + label: 'actions.viewParts', + icon: 'build', + action: (data: Record) => { + this.viewAssetsClicked.emit(data); + }, + }, + ], sortableColumns: { select: false, contractId: true, diff --git a/frontend/src/app/modules/page/parts/model/parts.model.ts b/frontend/src/app/modules/page/parts/model/parts.model.ts index 4296355d39..2e942e1a1f 100644 --- a/frontend/src/app/modules/page/parts/model/parts.model.ts +++ b/frontend/src/app/modules/page/parts/model/parts.model.ts @@ -42,6 +42,7 @@ export interface Part { owner: Owner; semanticDataModel: SemanticDataModel; classification: string; + contractAgreementId?: string; mainAspectType: MainAspectType; @@ -100,6 +101,7 @@ export interface PartResponse { receivedQualityInvestigationIdsInStatusActive: string[] importNote?: string, importState?: ImportState, + contractAgreementId?: string, tombstone?: string, } @@ -153,6 +155,7 @@ export interface AssetAsBuiltFilter { partId?: string, manufacturerPartId?: string, customerPartId?: string, + contractAgreementId?: string, classification?: string, nameAtCustomer?: string, semanticModelId?: string, @@ -170,6 +173,7 @@ export interface AssetAsPlannedFilter { businessPartner?: string, manufacturerPartId?: string, classification?: string, + contractAgreementId?: string, semanticDataModel?: string[], semanticModelId?: string, validityPeriodFrom?: string, diff --git a/frontend/src/app/modules/shared/assembler/parts.assembler.ts b/frontend/src/app/modules/shared/assembler/parts.assembler.ts index db8cbeebb3..1d6ebf7c04 100644 --- a/frontend/src/app/modules/shared/assembler/parts.assembler.ts +++ b/frontend/src/app/modules/shared/assembler/parts.assembler.ts @@ -89,6 +89,7 @@ export class PartsAssembler { semanticDataModel: partResponse.semanticDataModel, classification: partResponse.classification, semanticModel: createdSemanticModel, + contractAgreementId: partResponse.contractAgreementId, mainAspectType: mainAspectType, @@ -291,6 +292,7 @@ export class PartsAssembler { [ 'semanticDataModel', 'semanticDataModel' ], [ 'classification', 'classification' ], [ 'customerPartId', 'customerPartId' ], + [ 'contractAgreementId', 'contractAgreementId' ], [ 'nameAtCustomer', 'nameAtCustomer' ], [ 'manufacturingDate', 'manufacturingDate' ], [ 'manufacturingCountry', 'manufacturingCountry' ], diff --git a/frontend/src/app/modules/shared/components/multi-select-autocomplete/multi-select-autocomplete.component.spec.ts b/frontend/src/app/modules/shared/components/multi-select-autocomplete/multi-select-autocomplete.component.spec.ts index c281983241..e82a52275b 100644 --- a/frontend/src/app/modules/shared/components/multi-select-autocomplete/multi-select-autocomplete.component.spec.ts +++ b/frontend/src/app/modules/shared/components/multi-select-autocomplete/multi-select-autocomplete.component.spec.ts @@ -7,14 +7,14 @@ import { SharedModule } from '@shared/shared.module'; import { renderComponent } from '@tests/test-render.utils'; describe('MultiSelectAutocompleteComponent', () => { - const renderMultiSelectAutoCompleteComponent = (multiple = true) => { + const renderMultiSelectAutoCompleteComponent = (multiple = true, prefilterValue = null) => { const placeholder = 'test'; const options = [ SemanticDataModel.PARTASPLANNED, SemanticDataModel.BATCH ]; return renderComponent(MultiSelectAutocompleteComponent, { imports: [ SharedModule ], providers: [ DatePipe, FormatPartSemanticDataModelToCamelCasePipe ], - componentProperties: { placeholder: placeholder, options: options }, + componentProperties: { placeholder: placeholder, options: options, prefilterValue: prefilterValue }, }); }; @@ -265,6 +265,15 @@ describe('MultiSelectAutocompleteComponent', () => { expect(option).toEqual([]); }); + it('should set prefilter value', async () => { + const { fixture } = await renderMultiSelectAutoCompleteComponent(false, 'hello'); + const { componentInstance } = fixture; + + expect(componentInstance.searchElement).toEqual('hello'); + expect(componentInstance.selectedValue).toEqual([ 'hello' ]); + + }); + it('should return when calling filterItem() without value', async () => { const { fixture } = await renderMultiSelectAutoCompleteComponent(); const { componentInstance } = fixture; diff --git a/frontend/src/app/modules/shared/components/multi-select-autocomplete/multi-select-autocomplete.component.ts b/frontend/src/app/modules/shared/components/multi-select-autocomplete/multi-select-autocomplete.component.ts index 59d574c0d4..453c446046 100644 --- a/frontend/src/app/modules/shared/components/multi-select-autocomplete/multi-select-autocomplete.component.ts +++ b/frontend/src/app/modules/shared/components/multi-select-autocomplete/multi-select-autocomplete.component.ts @@ -106,6 +106,7 @@ export class MultiSelectAutocompleteComponent implements OnChanges { suggestionError: boolean = false; isLoadingSuggestions: boolean; + @Input() prefilterValue?: string; constructor(public datePipe: DatePipe, public _adapter: DateAdapter, @Inject(MAT_DATE_LOCALE) public _locale: string, @Inject(LOCALE_ID) private locale: string, public partsService: PartsService, @@ -125,6 +126,13 @@ export class MultiSelectAutocompleteComponent implements OnChanges { } }); + if (this.prefilterValue?.length > 0) { + this.searchElement = this.prefilterValue; + this.selectedValue = [ this.searchElement ]; + this.formControl.patchValue(this.selectedValue); + this.updateOptionsAndSelections(); + } + } ngOnChanges(): void { @@ -184,7 +192,7 @@ export class MultiSelectAutocompleteComponent implements OnChanges { filterItem(value: any): void { - if (!this.searchElement.length) { + if (!this.searchElement?.length) { return; } diff --git a/frontend/src/app/modules/shared/components/parts-table/parts-as-built-configuration.model.ts b/frontend/src/app/modules/shared/components/parts-table/parts-as-built-configuration.model.ts index d997f3245e..e8448f6b4b 100644 --- a/frontend/src/app/modules/shared/components/parts-table/parts-as-built-configuration.model.ts +++ b/frontend/src/app/modules/shared/components/parts-table/parts-as-built-configuration.model.ts @@ -42,6 +42,7 @@ export class PartsAsBuiltConfigurationModel extends TableFilterConfiguration { sentActiveInvestigations: true, importState: true, importNote: true, + contractAgreementId: true, menu: false, }; diff --git a/frontend/src/app/modules/shared/components/parts-table/parts-as-planned-configuration.model.ts b/frontend/src/app/modules/shared/components/parts-table/parts-as-planned-configuration.model.ts index 011068b42f..3c7bc22542 100644 --- a/frontend/src/app/modules/shared/components/parts-table/parts-as-planned-configuration.model.ts +++ b/frontend/src/app/modules/shared/components/parts-table/parts-as-planned-configuration.model.ts @@ -41,6 +41,7 @@ export class PartsAsPlannedConfigurationModel extends TableFilterConfiguration { functionValidUntil: true, importState: true, importNote: true, + contractAgreementId: true, menu: false, }; diff --git a/frontend/src/app/modules/shared/components/parts-table/parts-table.component.html b/frontend/src/app/modules/shared/components/parts-table/parts-table.component.html index c889fd4827..80c4249da0 100644 --- a/frontend/src/app/modules/shared/components/parts-table/parts-table.component.html +++ b/frontend/src/app/modules/shared/components/parts-table/parts-table.component.html @@ -135,6 +135,7 @@ [isDate]="filter.isDate" [singleSearch]="filter.singleSearch" [filterColumn]="filter.filterKey" + [prefilterValue]="filter.headerKey === 'filtercontractAgreementId' ? preFilter : null" [tableType]="tableType" [inAssetIds]="assetIdsForAutoCompleteFilter" [placeholderMultiple]="('multiSelect.multipleResults' | i18n)" diff --git a/frontend/src/app/modules/shared/components/parts-table/parts-table.component.spec.ts b/frontend/src/app/modules/shared/components/parts-table/parts-table.component.spec.ts index c901166e82..c3d1315501 100644 --- a/frontend/src/app/modules/shared/components/parts-table/parts-table.component.spec.ts +++ b/frontend/src/app/modules/shared/components/parts-table/parts-table.component.spec.ts @@ -140,6 +140,7 @@ describe('PartsTableComponent', () => { 'filtersentActiveInvestigations', 'filterimportState', 'filterimportNote', + 'filtercontractAgreementId', 'Menu', ]); }); @@ -170,6 +171,7 @@ describe('PartsTableComponent', () => { 'filterfunctionValidUntil', 'filterimportState', 'filterimportNote', + 'filtercontractAgreementId', 'Menu', ]); }); diff --git a/frontend/src/app/modules/shared/components/parts-table/parts-table.component.ts b/frontend/src/app/modules/shared/components/parts-table/parts-table.component.ts index 9ec976ecd5..673ab802c7 100644 --- a/frontend/src/app/modules/shared/components/parts-table/parts-table.component.ts +++ b/frontend/src/app/modules/shared/components/parts-table/parts-table.component.ts @@ -34,7 +34,7 @@ import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { MatPaginator, PageEvent } from '@angular/material/paginator'; import { MatSort, Sort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { EmptyPagination, Pagination } from '@core/model/pagination.model'; import { RoleService } from '@core/user/role.service'; import { TableSettingsService } from '@core/user/table-settings.service'; @@ -88,6 +88,7 @@ export class PartsTableComponent implements OnInit { @Input() assetIdsForAutoCompleteFilter: string[]; + preFilter: string; public tableConfig: TableConfig; @Input() set paginationData({ page, pageSize, totalItems, content }: Pagination) { @@ -134,9 +135,11 @@ export class PartsTableComponent implements OnInit { public readonly userSettingsService: BomLifecycleSettingsService, private dialog: MatDialog, private router: Router, + private route: ActivatedRoute, private deeplinkService: DeeplinkService, public roleService: RoleService, ) { + this.preFilter = route.snapshot.queryParams['contractId']; } handleKeyDownOpenDialog(event: KeyboardEvent) { diff --git a/frontend/src/app/modules/shared/components/table/table.component.html b/frontend/src/app/modules/shared/components/table/table.component.html index ed659cab80..83c43ef666 100644 --- a/frontend/src/app/modules/shared/components/table/table.component.html +++ b/frontend/src/app/modules/shared/components/table/table.component.html @@ -341,7 +341,17 @@

{{ 'table.noResultFound' | i18n }}

mat-menu-item > {{ config.icon }} +
{{ config.label | i18n }} +
diff --git a/frontend/src/assets/locales/de/common.json b/frontend/src/assets/locales/de/common.json index 7b2d43c7a2..9128f41455 100644 --- a/frontend/src/assets/locales/de/common.json +++ b/frontend/src/assets/locales/de/common.json @@ -69,7 +69,9 @@ "maximizeTable": "Volle Breite", "userSettings" : "Tabellen Einstellung", "uploadFile" : "Richtlinien JSON-Datei hochladen", - "downloadFile" : "Vorlage als Datei herunterladen" + "downloadFile" : "Vorlage als Datei herunterladen", + "viewParts" : "Produkte ansehen", + "viewPartsTooltip" : "Produkte mit diesem Vertrag ansehen" }, "publisher": { "selectedAssets": "Ausgewählte Produkte", @@ -175,7 +177,8 @@ "policyName" : "Richtlinienname", "bpn" : "BPN Selektion", "constraints" : "Bedingungen", - "accessType" : "Zugriffsart" + "accessType" : "Zugriffsart", + "contractAgreementId" : "Vertrags-ID" } }, "dataLoading": { diff --git a/frontend/src/assets/locales/en/common.json b/frontend/src/assets/locales/en/common.json index 2e49c3b6d3..db45a21c05 100644 --- a/frontend/src/assets/locales/en/common.json +++ b/frontend/src/assets/locales/en/common.json @@ -67,7 +67,9 @@ "maximizeTable": "Full width", "userSettings" : "Table settings", "uploadFile" : "Upload policy JSON file", - "downloadFile" : "Download template as file" + "downloadFile" : "Download template as file", + "viewParts" : "View parts", + "viewPartsTooltip" : "View parts using this contract" }, "publisher": { "selectedAssets": "Assets selected", @@ -171,7 +173,8 @@ "policyName" : "Policy name", "bpn" : "BPN selection", "constraints" : "Constraints", - "accessType" : "Access type" + "accessType" : "Access type", + "contractAgreementId" : "Contract ID" } }, "dataLoading": {