diff --git a/libs/angular-accelerator/src/index.ts b/libs/angular-accelerator/src/index.ts index 9d8ae9d3..87ae2c0a 100644 --- a/libs/angular-accelerator/src/index.ts +++ b/libs/angular-accelerator/src/index.ts @@ -17,14 +17,12 @@ export * from './lib/components/diagram/diagram.component' export * from './lib/components/group-by-count-diagram/group-by-count-diagram.component' export * from './lib/components/interactive-data-view/interactive-data-view.component' export * from './lib/components/page-header/page-header.component' -export * from './lib/components/search-config/search-config.component' export * from './lib/components/search-header/search-header.component' export * from './lib/components/data-loading-error/data-loading-error.component' // services export * from './lib/services/breadcrumb.service' export * from './lib/services/translation-cache.service' -export * from './lib/services/app-config-service' // pipes export * from './lib/pipes/dynamic.pipe' @@ -40,7 +38,6 @@ export * from './lib/model/data-table-column.model' export * from './lib/model/diagram-column' // export * from './lib/model/diagram-data' export * from './lib/model/diagram-type' -export * from './lib/model/search-config-info' // core export * from './lib/angular-accelerator.module' @@ -59,6 +56,7 @@ export * from './lib/utils/dateutils' export * from './lib/utils/objectutils' export * from './lib/utils/primeicon.utils' export * from './lib/utils/translate.combined.loader' +export * from './lib/utils/create-remote-component-and-mfe-translate-loader.utils' export * from './lib/utils/create-remote-component-translate-loader.utils' export * from './lib/utils/enum-to-dropdown-options.utils' export * from './lib/utils/criteria.utils' diff --git a/libs/angular-accelerator/src/lib/angular-accelerator.module.ts b/libs/angular-accelerator/src/lib/angular-accelerator.module.ts index a9291f73..de1e8bd1 100644 --- a/libs/angular-accelerator/src/lib/angular-accelerator.module.ts +++ b/libs/angular-accelerator/src/lib/angular-accelerator.module.ts @@ -4,7 +4,8 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { RouterModule } from '@angular/router' import { MissingTranslationHandler, MissingTranslationHandlerParams, TranslateModule } from '@ngx-translate/core' -import { UserService } from '@onecx/angular-integration-interface' +import { AppConfigService, UserService } from '@onecx/angular-integration-interface' +import { AngularRemoteComponentsModule } from '@onecx/angular-remote-components' import { firstValueFrom, skip } from 'rxjs' import { AngularAcceleratorPrimeNgModule } from './angular-accelerator-primeng.module' @@ -20,7 +21,6 @@ import { GroupByCountDiagramComponent } from './components/group-by-count-diagra import { InteractiveDataViewComponent } from './components/interactive-data-view/interactive-data-view.component' import { PageHeaderComponent } from './components/page-header/page-header.component' import { DataLoadingErrorComponent } from './components/data-loading-error/data-loading-error.component' -import { SearchConfigComponent } from './components/search-config/search-config.component' import { SearchHeaderComponent } from './components/search-header/search-header.component' import { AdvancedDirective } from './directives/advanced.directive' import { IfBreakpointDirective } from './directives/if-breakpoint.directive' @@ -29,7 +29,6 @@ import { SrcDirective } from './directives/src.directive' import { TooltipOnOverflowDirective } from './directives/tooltipOnOverflow.directive' import { DynamicPipe } from './pipes/dynamic.pipe' import { OcxTimeAgoPipe } from './pipes/ocxtimeago.pipe' -import { AppConfigService } from './services/app-config-service' import { DynamicLocaleId } from './utils/dynamic-locale-id' export class AngularAcceleratorMissingTranslationHandler implements MissingTranslationHandler { @@ -49,6 +48,7 @@ function appInitializer(userService: UserService) { imports: [ CommonModule, AngularAcceleratorPrimeNgModule, + AngularRemoteComponentsModule, TranslateModule, FormsModule, RouterModule, @@ -63,7 +63,6 @@ function appInitializer(userService: UserService) { DataTableComponent, DataViewComponent, InteractiveDataViewComponent, - SearchConfigComponent, PageHeaderComponent, DynamicPipe, SearchHeaderComponent, @@ -96,6 +95,7 @@ function appInitializer(userService: UserService) { AppConfigService, ], exports: [ + AngularRemoteComponentsModule, ColumnGroupSelectionComponent, CustomGroupColumnSelectorComponent, DataLayoutSelectionComponent, @@ -103,7 +103,6 @@ function appInitializer(userService: UserService) { DataTableComponent, DataViewComponent, InteractiveDataViewComponent, - SearchConfigComponent, PageHeaderComponent, SearchHeaderComponent, DiagramComponent, diff --git a/libs/angular-accelerator/src/lib/components/interactive-data-view/interactive-data-view.component.html b/libs/angular-accelerator/src/lib/components/interactive-data-view/interactive-data-view.component.html index d8377188..68355773 100644 --- a/libs/angular-accelerator/src/lib/components/interactive-data-view/interactive-data-view.component.html +++ b/libs/angular-accelerator/src/lib/components/interactive-data-view/interactive-data-view.component.html @@ -23,18 +23,32 @@ > -
- +
+ + + + +
+ +
+
+
+
+
+ + + + + { const mutationObserverMock = jest.fn(function MutationObserver(callback) { @@ -63,6 +67,7 @@ describe('InteractiveDataViewComponent', () => { let deleteItemEvent: RowListGridData | undefined let dateUtils: DateUtils + let slotService: SlotServiceMock const mockData = [ { @@ -229,6 +234,7 @@ describe('InteractiveDataViewComponent', () => { DataViewComponent, ColumnGroupSelectionComponent, CustomGroupColumnSelectorComponent, + IfPermissionDirective, ], imports: [ TranslateModule.forRoot(), @@ -244,6 +250,10 @@ describe('InteractiveDataViewComponent', () => { ], providers: [ { provide: UserService, useClass: MockUserService }, + { + provide: SlotService, + useClass: SlotServiceMock, + }, provideHttpClient(withInterceptorsFromDi()), provideRouter([]), provideAppStateServiceMock(), @@ -256,6 +266,7 @@ describe('InteractiveDataViewComponent', () => { component.editPermission = 'TEST_MGMT#TEST_EDIT' component.deletePermission = 'TEST_MGMT#TEST_DELETE' component.defaultGroupKey = 'PREDEFINED_GROUP.DEFAULT' + component.searchConfigPermission = 'PRODUCT#USE_SEARCHCONFIG' component.viewItem.subscribe((event) => (viewItemEvent = event)) component.editItem.subscribe((event) => (editItemEvent = event)) component.deleteItem.subscribe((event) => (deleteItemEvent = event)) @@ -270,6 +281,7 @@ describe('InteractiveDataViewComponent', () => { interactiveDataViewHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, InteractiveDataViewHarness) dateUtils = TestBed.inject(DateUtils) + slotService = TestBed.inject(SlotService) as any as SlotServiceMock viewItemEvent = undefined editItemEvent = undefined @@ -290,9 +302,26 @@ describe('InteractiveDataViewComponent', () => { expect(dataLayoutSelection).toBeTruthy() }) + it('should load column-group-selection slot', async () => { + slotService.assignComponentToSlot('column-group-selection', component.columnGroupSlotName) + const userService = TestBed.inject(UserService) + jest.spyOn(userService, 'hasPermission').mockReturnValue(true) + fixture.detectChanges() + + const slot = await loader.getHarness(SlotHarness) + expect(slot).toBeTruthy() + }) + it('should load ColumnGroupSelectionDropdown', async () => { const columnGroupSelectionDropdown = await loader.getHarness(ColumnGroupSelectionHarness) expect(columnGroupSelectionDropdown).toBeTruthy() + + slotService.assignComponentToSlot('column-group-selection', component.columnGroupSlotName) + const userService = TestBed.inject(UserService) + jest.spyOn(userService, 'hasPermission').mockReturnValue(false) + + const columnGroupSelectionDropdownNoPermission = await loader.getHarness(ColumnGroupSelectionHarness) + expect(columnGroupSelectionDropdownNoPermission).toBeTruthy() }) it('should load CustomGroupColumnSelector', async () => { @@ -933,18 +962,18 @@ describe('InteractiveDataViewComponent', () => { it('should move item up in picklist active columns list', async () => { const spy = jest.spyOn(CustomGroupColumnSelectorComponent.prototype, 'onSaveClick') const expectedHeaders = [ - 'COLUMN_HEADER_NAME.DESCRIPTION', 'COLUMN_HEADER_NAME.NAME', + 'COLUMN_HEADER_NAME.DESCRIPTION', 'COLUMN_HEADER_NAME.STATUS', 'COLUMN_HEADER_NAME.RESPONSIBLE', 'Actions', ] const expectedRowsData = [ - ['', 'some name', 'some status', 'someone responsible'], - ['example description', 'example', 'status example', ''], - ['', 'name 1', 'status name 1', ''], - ['', 'name 2', 'status name 2', ''], - ['', 'name 3', 'status name 3', ''], + ['some name', '', 'some status', 'someone responsible'], + ['example', 'example description', 'status example', ''], + ['name 1', '', 'status name 1', ''], + ['name 2', '', 'status name 2', ''], + ['name 3', '', 'status name 3', ''], ] await activeColumnsList[1].selectItem() await sourceControlsButtons[0].click() @@ -963,17 +992,17 @@ describe('InteractiveDataViewComponent', () => { const spy = jest.spyOn(CustomGroupColumnSelectorComponent.prototype, 'onSaveClick') const expectedHeaders = [ 'COLUMN_HEADER_NAME.NAME', - 'COLUMN_HEADER_NAME.STATUS', 'COLUMN_HEADER_NAME.DESCRIPTION', + 'COLUMN_HEADER_NAME.STATUS', 'COLUMN_HEADER_NAME.RESPONSIBLE', 'Actions', ] const expectedRowsData = [ - ['some name', 'some status', '', 'someone responsible'], - ['example', 'status example', 'example description', ''], - ['name 1', 'status name 1', '', ''], - ['name 2', 'status name 2', '', ''], - ['name 3', 'status name 3', '', ''], + ['some name', '', 'some status', 'someone responsible'], + ['example', 'example description', 'status example', ''], + ['name 1', '', 'status name 1', ''], + ['name 2', '', 'status name 2', ''], + ['name 3', '', 'status name 3', ''], ] await activeColumnsList[1].selectItem() @@ -1698,4 +1727,26 @@ describe('InteractiveDataViewComponent', () => { }) }) }) + + it('should react on group selection change event emit', () => { + const columnsChangeSpy = jest.spyOn(component.displayedColumnsChange, 'emit') + const columnKeysChangeSpy = jest.spyOn(component.displayedColumnKeysChange, 'emit') + + component.groupSelectionChangedSlotEmitter.emit({ + activeColumns: [ + { + id: 'first-col', + } as any, + { + id: 'second-col', + } as any, + ], + groupKey: 'my-search-config', + }) + + expect(component.displayedColumnKeys).toStrictEqual(['first-col', 'second-col']) + expect(component.selectedGroupKey).toBe('my-search-config') + expect(columnsChangeSpy).toHaveBeenCalled() + expect(columnKeysChangeSpy).toHaveBeenCalledWith(['first-col', 'second-col']) + }) }) diff --git a/libs/angular-accelerator/src/lib/components/interactive-data-view/interactive-data-view.component.ts b/libs/angular-accelerator/src/lib/components/interactive-data-view/interactive-data-view.component.ts index ee4b06e2..802f4264 100644 --- a/libs/angular-accelerator/src/lib/components/interactive-data-view/interactive-data-view.component.ts +++ b/libs/angular-accelerator/src/lib/components/interactive-data-view/interactive-data-view.component.ts @@ -11,7 +11,17 @@ import { TemplateRef, ViewChild, } from '@angular/core' -import { BehaviorSubject, Observable, ReplaySubject, combineLatest, map, startWith, timestamp } from 'rxjs' +import { + BehaviorSubject, + Observable, + ReplaySubject, + combineLatest, + distinctUntilChanged, + map, + startWith, + timestamp, + withLatestFrom, +} from 'rxjs' import { DataAction } from '../../model/data-action' import { DataSortDirection } from '../../model/data-sort-direction' import { DataTableColumn } from '../../model/data-table-column.model' @@ -26,6 +36,7 @@ import { ColumnSelectionChangedEvent, CustomGroupColumnSelectorComponentState, } from '../custom-group-column-selector/custom-group-column-selector.component' +import { SlotService } from '@onecx/angular-remote-components' import { DataLayoutSelectionComponentState } from '../data-layout-selection/data-layout-selection.component' import { DataListGridSortingComponentState } from '../data-list-grid-sorting/data-list-grid-sorting.component' import { Filter, Row, Sort } from '../data-table/data-table.component' @@ -36,6 +47,11 @@ export type InteractiveDataViewComponentState = ColumnGroupSelectionComponentSta DataLayoutSelectionComponentState & DataListGridSortingComponentState & DataViewComponentState + +export interface ColumnGroupData { + activeColumns: DataTableColumn[] + groupKey: string +} @Component({ selector: 'ocx-interactive-data-view', templateUrl: './interactive-data-view.component.html', @@ -58,6 +74,7 @@ export class InteractiveDataViewComponent implements OnInit, AfterContentInit { dataListGridSortingComponentState$ = new ReplaySubject(1) dataViewComponentState$ = new ReplaySubject(1) + @Input() searchConfigPermission: string | undefined @Input() deletePermission: string | undefined @Input() editPermission: string | undefined @Input() viewPermission: string | undefined @@ -102,7 +119,7 @@ export class InteractiveDataViewComponent implements OnInit, AfterContentInit { @Input() selectedRows: Row[] = [] displayedColumnKeys$ = new BehaviorSubject([]) displayedColumns$: Observable | undefined - @Input() + @Input() get displayedColumnKeys(): string[] { return this.displayedColumnKeys$.getValue() } @@ -114,11 +131,11 @@ export class InteractiveDataViewComponent implements OnInit, AfterContentInit { */ @Input() get displayedColumns(): DataTableColumn[] { - return ( - (this.displayedColumnKeys - .map((d) => this.columns.find((c) => c.id === d)) - .filter((d) => d) as DataTableColumn[]) ?? [] - ); + return ( + (this.displayedColumnKeys + .map((d) => this.columns.find((c) => c.id === d)) + .filter((d) => d) as DataTableColumn[]) ?? [] + ) } set displayedColumns(value: DataTableColumn[]) { this.displayedColumnKeys$.next(value.map((d) => d.id)) @@ -206,7 +223,13 @@ export class InteractiveDataViewComponent implements OnInit, AfterContentInit { @Output() componentStateChanged = new EventEmitter() - selectedGroupKey = '' + selectedGroupKey$ = new BehaviorSubject('') + get selectedGroupKey(): string | undefined { + return this.selectedGroupKey$.getValue() + } + set selectedGroupKey(value: string | undefined) { + this.selectedGroupKey$.next(value) + } isDeleteItemObserved: boolean | undefined isViewItemObserved: boolean | undefined isEditItemObserved: boolean | undefined @@ -315,22 +338,66 @@ export class InteractiveDataViewComponent implements OnInit, AfterContentInit { this._data = value } + columnGroupSlotName = 'onecx-shell-column-group-selection' + isColumnGroupSelectionComponentDefined$: Observable + groupSelectionChangedSlotEmitter = new EventEmitter() + + constructor(private slotService: SlotService) { + this.isColumnGroupSelectionComponentDefined$ = this.slotService + .isSomeComponentDefinedForSlot(this.columnGroupSlotName) + .pipe(startWith(true)) + + this.groupSelectionChangedSlotEmitter.subscribe((event: ColumnGroupData | undefined) => { + if (event === undefined) { + event = { + activeColumns: this.displayedColumns, + groupKey: this.selectedGroupKey ?? this.defaultGroupKey, + } + } + this.displayedColumnKeys$.next(event.activeColumns.map((col) => col.id)) + this.selectedGroupKey$.next(event.groupKey) + this.displayedColumnsChange.emit(this.displayedColumns) + this.displayedColumnKeysChange.emit(this.displayedColumnKeys) + this.columnGroupSelectionComponentState$.next({ + activeColumnGroupKey: event.groupKey, + displayedColumns: event.activeColumns, + }) + }) + + this.dataViewLayoutChange + .pipe(withLatestFrom(this.isColumnGroupSelectionComponentDefined$)) + .subscribe(([_, columnGroupComponentDefined]) => { + if (columnGroupComponentDefined) { + if ( + !( + this.columns.find((c) => c.nameKey === this.selectedGroupKey) || + this.selectedGroupKey === this.customGroupKey + ) + ) { + this.selectedGroupKey$.next(undefined) + } + } + }) + } + ngOnInit(): void { this.selectedGroupKey = this.defaultGroupKey - if(!this.displayedColumns || this.displayedColumns.length === 0) { + if (!this.displayedColumns || this.displayedColumns.length === 0) { this.displayedColumnKeys = this.columns.map((column) => column.id) } if (this.defaultGroupKey) { - this.displayedColumnKeys = this.columns.filter((column) => - column.predefinedGroupKeys?.includes(this.defaultGroupKey) - ).map((column) => column.id) + this.displayedColumnKeys = this.columns + .filter((column) => column.predefinedGroupKeys?.includes(this.defaultGroupKey)) + .map((column) => column.id) } - this.displayedColumns$ = this.displayedColumnKeys$.pipe(map((columnKeys) => ( - (columnKeys - .map((key) => this.columns.find((col) => col.id === key)) - .filter((d) => d) as DataTableColumn[]) ?? [] - ))) - // TODO: Remove following line once displayedColumns (deprecated) has been removed + this.displayedColumns$ = this.displayedColumnKeys$.pipe( + distinctUntilChanged((prev, curr) => prev.length === curr.length && prev.every((v) => curr.includes(v))), + map( + (columnKeys) => + (columnKeys.map((key) => this.columns.find((col) => col.id === key)).filter((d) => d) as DataTableColumn[]) ?? + [] + ) + ) this.displayedColumnsChange.emit(this.displayedColumns) this.displayedColumnKeysChange.emit(this.displayedColumnKeys) if (!this.groupSelectionNoGroupSelectedKey) { @@ -340,10 +407,11 @@ export class InteractiveDataViewComponent implements OnInit, AfterContentInit { let dataListGridSortingComponentState$: Observable> = this.dataListGridSortingComponentState$ - let columnGroupSelectionComponentState$: Observable> = + let columnGroupSelectionComponentState$: Observable> = this.columnGroupSelectionComponentState$ - let customGroupColumnSelectorComponentState$: Observable> = - this.customGroupColumnSelectorComponentState$ + let customGroupColumnSelectorComponentState$: Observable< + CustomGroupColumnSelectorComponentState | Record + > = this.customGroupColumnSelectorComponentState$ if (this.layout === 'table') { dataListGridSortingComponentState$ = dataListGridSortingComponentState$.pipe(startWith({})) @@ -577,5 +645,4 @@ export class InteractiveDataViewComponent implements OnInit, AfterContentInit { this.pageSize = event this.pageSizeChanged.emit(event) } - } diff --git a/libs/angular-accelerator/src/lib/components/search-config/search-config.component.html b/libs/angular-accelerator/src/lib/components/search-config/search-config.component.html deleted file mode 100644 index 767142e7..00000000 --- a/libs/angular-accelerator/src/lib/components/search-config/search-config.component.html +++ /dev/null @@ -1,17 +0,0 @@ -
-
-
-
- -
-
-
-
diff --git a/libs/angular-accelerator/src/lib/components/search-config/search-config.component.scss b/libs/angular-accelerator/src/lib/components/search-config/search-config.component.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/libs/angular-accelerator/src/lib/components/search-config/search-config.component.spec.ts b/libs/angular-accelerator/src/lib/components/search-config/search-config.component.spec.ts deleted file mode 100644 index 6b3a4022..00000000 --- a/libs/angular-accelerator/src/lib/components/search-config/search-config.component.spec.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing' -import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed' -import { ReactiveFormsModule } from '@angular/forms' -import { NoopAnimationsModule } from '@angular/platform-browser/animations' -import { provideHttpClientTesting } from '@angular/common/http/testing' -import { TranslateService } from '@ngx-translate/core' -import { TranslateTestingModule } from 'ngx-translate-testing' -import { MessageModule } from 'primeng/message' -import { AngularAcceleratorPrimeNgModule } from '../../angular-accelerator-primeng.module' -import { SearchConfigComponent } from './search-config.component' -import { SearchConfigHarness } from '../../../../testing' -import { SearchConfigInfo } from '../../model/search-config-info' -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; - -describe('SearchConfigComponent', () => { - let translateService: TranslateService - let component: SearchConfigComponent - let fixture: ComponentFixture - - const searchConfigs: SearchConfigInfo[] = [ - { - id: '01', - name: 'Simple search config', - }, - { - id: '02', - name: 'Adapted search config', - }, - ] - - const emptySearchConfigEntry: SearchConfigInfo[] = [] - - const placeholderKey = 'OCX_SEARCH_HEADER.OCX_SEARCH_CONFIG.DROPDOWN_DEFAULT' - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [SearchConfigComponent], - imports: [NoopAnimationsModule, - MessageModule, - TranslateTestingModule.withTranslations({ - en: require('./../../../../assets/i18n/en.json'), - de: require('./../../../../assets/i18n/de.json'), - }), - AngularAcceleratorPrimeNgModule, - ReactiveFormsModule], - providers: [ - { - useValue: { - baseHref: '/base/path', - mountPath: '/base/path', - remoteBaseUrl: 'http://localhost:4200', - shellName: 'shell', - }, - }, - provideHttpClient(withInterceptorsFromDi()), - provideHttpClientTesting(), - ] -}).compileComponents() - - fixture = TestBed.createComponent(SearchConfigComponent) - component = fixture.componentInstance - component.searchConfigs = searchConfigs - translateService = TestBed.inject(TranslateService) - translateService.setDefaultLang('en') - translateService.use('en') - fixture.detectChanges() - }) - - it('should create', () => { - expect(component).toBeTruthy() - }) - - it('should load the PDropdownHarness', async () => { - const searchConfigHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, SearchConfigHarness) - const dropdown = await searchConfigHarness.getSearchConfigDropdown() - expect(dropdown).toBeTruthy() - }) - - it('should open the dropdown', async () => { - const searchConfigHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, SearchConfigHarness) - const dropdown = await searchConfigHarness.getSearchConfigDropdown() - await dropdown?.open() - expect(await dropdown?.isOpen()).toBeTruthy() - }) - - it('should display a dropdown with a hard coded search config', async () => { - const searchConfigHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, SearchConfigHarness) - const dropdown = await searchConfigHarness.getSearchConfigDropdown() - const items = await dropdown?.getDropdownItems() - expect(items?.length).toEqual(searchConfigs.length) - }) - - it('should display no dropdown if the search config is empty', async () => { - component.searchConfigs = emptySearchConfigEntry - const searchConfigHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, SearchConfigHarness) - const dropdown = await searchConfigHarness.getSearchConfigDropdown() - expect(dropdown).toBeFalsy() - }) - - it('should display the values in the fields after selecting the fist hard coded search config', async () => { - const searchConfigHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, SearchConfigHarness) - const dropdown = await searchConfigHarness.getSearchConfigDropdown() - const selectedDropdownItem = await dropdown?.selectedDropdownItemText(0) - expect(selectedDropdownItem).toEqual(searchConfigs[0].name) - }) - - it('should display the values in the fields after selecting the fist hard coded search config', async () => { - const searchConfigHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, SearchConfigHarness) - const dropdown = await searchConfigHarness.getSearchConfigDropdown() - const selectedDropdownItem = await dropdown?.selectedDropdownItemText(1) - expect(selectedDropdownItem).toEqual(searchConfigs[1].name) - }) - - it('should display the values in the fields correctly after selecting the fist search config and then selecting the second search config', async () => { - const searchConfigHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, SearchConfigHarness) - const dropdown = await searchConfigHarness.getSearchConfigDropdown() - let selectedDropdownItem = await dropdown?.selectedDropdownItemText(0) - selectedDropdownItem = await dropdown?.selectedDropdownItemText(1) - expect(selectedDropdownItem).toEqual(searchConfigs[1].name) - }) - - it('should have the option to remove the selection', async () => { - const searchConfigHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, SearchConfigHarness) - const dropdown = await searchConfigHarness.getSearchConfigDropdown() - expect(await dropdown?.hasClearOption()).toBeTruthy() - }) - - it('should display the right default message', async () => { - const searchConfigHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, SearchConfigHarness) - const dropdown = await searchConfigHarness.getSearchConfigDropdown() - const definedDefaultKeyTranslation = translateService.instant(placeholderKey) - expect(await dropdown?.getDefaultText()).toEqual(definedDefaultKeyTranslation) - }) -}) diff --git a/libs/angular-accelerator/src/lib/components/search-config/search-config.component.ts b/libs/angular-accelerator/src/lib/components/search-config/search-config.component.ts deleted file mode 100644 index d5ca6ff5..00000000 --- a/libs/angular-accelerator/src/lib/components/search-config/search-config.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' -import { FormControl, FormGroup } from '@angular/forms' -import { SearchConfigInfo } from '../../model/search-config-info' - -@Component({ - selector: 'ocx-search-config', - templateUrl: './search-config.component.html', - styleUrls: ['./search-config.component.scss'], -}) -export class SearchConfigComponent implements OnInit { - @Input() - searchConfigs: SearchConfigInfo[] | [] | undefined - - @Input() placeholderKey = 'OCX_SEARCH_HEADER.OCX_SEARCH_CONFIG.DROPDOWN_DEFAULT' - - @Output() - selectedSearchConfigChanged: EventEmitter = new EventEmitter() - - formGroup: FormGroup | undefined - ngOnInit(): void { - this.formGroup = new FormGroup({ - searchConfigForm: new FormControl(null), - }) - } - - onSearchConfigChange(event: { value: SearchConfigInfo }) { - this.selectedSearchConfigChanged?.emit(event.value) - } -} diff --git a/libs/angular-accelerator/src/lib/components/search-header/search-header.component.html b/libs/angular-accelerator/src/lib/components/search-header/search-header.component.html index fb6026a9..5183eb11 100644 --- a/libs/angular-accelerator/src/lib/components/search-header/search-header.component.html +++ b/libs/angular-accelerator/src/lib/components/search-header/search-header.component.html @@ -5,8 +5,20 @@ [actions]="headerActions" > - - + + + +
+ +
+
+
+
diff --git a/libs/angular-accelerator/src/lib/components/search-header/search-header.component.spec.ts b/libs/angular-accelerator/src/lib/components/search-header/search-header.component.spec.ts index 3cd940a9..36d2acda 100644 --- a/libs/angular-accelerator/src/lib/components/search-header/search-header.component.spec.ts +++ b/libs/angular-accelerator/src/lib/components/search-header/search-header.component.spec.ts @@ -4,21 +4,26 @@ import { provideHttpClientTesting } from '@angular/common/http/testing' import { TranslateTestingModule } from 'ngx-translate-testing' import { ButtonModule } from 'primeng/button' import { BreadcrumbModule } from 'primeng/breadcrumb' -import { AppStateService } from '@onecx/angular-integration-interface' +import { AppStateService, UserService } from '@onecx/angular-integration-interface' import { AngularAcceleratorModule } from '../../angular-accelerator.module' import { SearchHeaderComponent } from './search-header.component' import { PageHeaderComponent } from '../page-header/page-header.component' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' import { AppStateServiceMock, provideAppStateServiceMock } from '@onecx/angular-integration-interface/mocks' +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed' +import { HarnessLoader } from '@angular/cdk/testing' +import { SlotHarness } from '@onecx/angular-accelerator/testing' +import { IfPermissionDirective } from '../../directives/if-permission.directive' describe('SearchHeaderComponent', () => { let mockAppStateService: AppStateServiceMock let component: SearchHeaderComponent let fixture: ComponentFixture + let loader: HarnessLoader beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [SearchHeaderComponent, PageHeaderComponent], + declarations: [SearchHeaderComponent, PageHeaderComponent, IfPermissionDirective], imports: [ TranslateTestingModule.withTranslations({}), RouterTestingModule, @@ -46,9 +51,30 @@ describe('SearchHeaderComponent', () => { fixture = TestBed.createComponent(SearchHeaderComponent) component = fixture.componentInstance fixture.detectChanges() + + loader = TestbedHarnessEnvironment.loader(fixture) }) it('should create', () => { expect(component).toBeTruthy() }) + + it('should not display search config slot if search config change is not observed', async () => { + const slot = await loader.getHarnessOrNull(SlotHarness) + expect(slot).toBeFalsy() + }) + + it('should display search config slot if search config change is observed, pageName is defined and permission is met', async () => { + const userService = TestBed.inject(UserService) + jest.spyOn(userService, 'hasPermission').mockReturnValue(true) + const sub = component.selectedSearchConfigChanged.subscribe() + component.pageName = 'myPageName' + component.searchConfigPermission = 'PRODUCT#USE_SEARCHCONFIGS' + fixture.detectChanges() + + const slot = await loader.getHarness(SlotHarness) + expect(slot).toBeTruthy() + + sub.unsubscribe() + }) }) diff --git a/libs/angular-accelerator/src/lib/components/search-header/search-header.component.ts b/libs/angular-accelerator/src/lib/components/search-header/search-header.component.ts index 55538fdb..f5da7ae6 100644 --- a/libs/angular-accelerator/src/lib/components/search-header/search-header.component.ts +++ b/libs/angular-accelerator/src/lib/components/search-header/search-header.component.ts @@ -1,20 +1,32 @@ import { + AfterContentInit, AfterViewInit, Component, ContentChild, + ContentChildren, ElementRef, EventEmitter, Input, Output, + QueryList, TemplateRef, ViewChild, } from '@angular/core' import { Action } from '../page-header/page-header.component' -import { SearchConfigInfo } from '../../model/search-config-info' +import { FormControlName, FormGroup, FormGroupDirective } from '@angular/forms' +import { Observable, combineLatest, debounceTime, map, of, startWith } from 'rxjs' +import { getLocation } from '@onecx/accelerator' export interface SearchHeaderComponentState { activeViewMode?: 'basic' | 'advanced' - selectedSearchConfig?: SearchConfigInfo + selectedSearchConfig?: string | null +} + +export interface SearchConfigData { + name: string | undefined + fieldValues: { [key: string]: string } + displayedColumnsIds: string[] + viewMode: 'basic' | 'advanced' } /** @@ -26,9 +38,9 @@ export interface SearchHeaderComponentState { @Component({ selector: 'ocx-search-header', templateUrl: './search-header.component.html', + providers: [], }) -export class SearchHeaderComponent implements AfterViewInit { - @Input() searchConfigs: SearchConfigInfo[] | undefined +export class SearchHeaderComponent implements AfterContentInit, AfterViewInit { @Input() header = '' /** @@ -42,7 +54,22 @@ export class SearchHeaderComponent implements AfterViewInit { this.header = value } @Input() subheader: string | undefined - @Input() viewMode: 'basic' | 'advanced' = 'basic' + _viewMode: 'basic' | 'advanced' = 'basic' + @Input() + get viewMode(): 'basic' | 'advanced' { + return this._viewMode + } + set viewMode(viewMode: 'basic' | 'advanced') { + if (this.viewMode !== viewMode) { + this._viewMode = viewMode + this.viewModeChanged?.emit(this.viewMode) + this.componentStateChanged.emit({ + activeViewMode: this.viewMode, + }) + this.updateHeaderActions() + setTimeout(() => this.addKeyUpEventListener()) + } + } @Input() manualBreadcrumbs = false _actions: Action[] = [] @Input() @@ -53,12 +80,14 @@ export class SearchHeaderComponent implements AfterViewInit { this._actions = value this.updateHeaderActions() } + @Input() searchConfigPermission: string | undefined @Input() searchButtonDisabled = false @Input() resetButtonDisabled = false + @Input() pageName: string | undefined = getLocation().applicationPath @Output() searched: EventEmitter = new EventEmitter() @Output() resetted: EventEmitter = new EventEmitter() - @Output() selectedSearchConfigChanged: EventEmitter = new EventEmitter() + @Output() selectedSearchConfigChanged: EventEmitter = new EventEmitter() @Output() viewModeChanged: EventEmitter<'basic' | 'advanced'> = new EventEmitter() @Output() componentStateChanged: EventEmitter = new EventEmitter() @ContentChild('additionalToolbarContent') @@ -74,23 +103,56 @@ export class SearchHeaderComponent implements AfterViewInit { return this.additionalToolbarContentLeft } + get searchConfigChangeObserved(): boolean { + return this.selectedSearchConfigChanged.observed + } + + @ContentChild(FormGroupDirective) formGroup: FormGroup | undefined + @ContentChildren(FormControlName, { descendants: true }) visibleFormControls!: QueryList + @ViewChild('searchParameterFields') searchParameterFields: ElementRef | undefined hasAdvanced = false headerActions: Action[] = [] + fieldValues$: Observable<{ [key: string]: unknown }> | undefined = of({}) + searchConfigChangedSlotEmitter: EventEmitter = new EventEmitter() + + constructor() { + this.searchConfigChangedSlotEmitter.subscribe((config) => { + this.componentStateChanged.emit({ + selectedSearchConfig: config?.name ?? null, + }) + this.selectedSearchConfigChanged.emit(config) + }) + } + + ngAfterContentInit(): void { + if (this.formGroup) { + this.fieldValues$ = combineLatest([ + this.formGroup.valueChanges.pipe(startWith({})), + this.visibleFormControls.changes.pipe(startWith(null)), + ]).pipe( + debounceTime(100), + map(([values, _]) => + Object.entries(values ?? {}).reduce( + (acc, [key, value]) => ({ + ...acc, + [key]: this.isVisible(key) ? value || undefined : undefined, + }), + {} + ) + ) + ) + } + } + ngAfterViewInit(): void { this.addKeyUpEventListener() } toggleViewMode() { this.viewMode = this.viewMode === 'basic' ? 'advanced' : 'basic' - this.viewModeChanged?.emit(this.viewMode) - this.componentStateChanged.emit({ - activeViewMode: this.viewMode - }) - this.updateHeaderActions() - setTimeout(() => this.addKeyUpEventListener()) } onResetClicked() { @@ -137,10 +199,9 @@ export class SearchHeaderComponent implements AfterViewInit { } } - confirmSearchConfig(searchConfig: SearchConfigInfo) { - this.selectedSearchConfigChanged?.emit(searchConfig) - this.componentStateChanged.emit({ - selectedSearchConfig: searchConfig - }) + private isVisible(control: string) { + return this.visibleFormControls.some( + (formControl) => formControl.name !== null && String(formControl.name) === control + ) } } diff --git a/libs/angular-accelerator/src/lib/components/search-header/search-header.stories.ts b/libs/angular-accelerator/src/lib/components/search-header/search-header.stories.ts index d59c3a99..35e81df6 100644 --- a/libs/angular-accelerator/src/lib/components/search-header/search-header.stories.ts +++ b/libs/angular-accelerator/src/lib/components/search-header/search-header.stories.ts @@ -13,7 +13,6 @@ import { SkeletonModule } from 'primeng/skeleton' import { DynamicPipe } from '../../pipes/dynamic.pipe' import { StorybookTranslateModule } from '../../storybook-translate.module' import { PageHeaderComponent } from '../page-header/page-header.component' -import { SearchConfigComponent } from '../search-config/search-config.component' import { StorybookBreadcrumbModule } from './../../storybook-breadcrumb.module' import { SearchHeaderComponent } from './search-header.component' @@ -29,7 +28,7 @@ export default { ], }), moduleMetadata({ - declarations: [SearchHeaderComponent, DynamicPipe, PageHeaderComponent, SearchConfigComponent], + declarations: [SearchHeaderComponent, DynamicPipe, PageHeaderComponent], imports: [ MenuModule, InputTextModule, @@ -115,11 +114,11 @@ const BasicSearchHeader: StoryFn = (args) => ({ }) export const WithCustomTemplates = { - render: BasicSearchHeader, - argTypes: { - resetted: { action: 'resetted' }, - }, - args: { - header: 'My title', - }, - } \ No newline at end of file + render: BasicSearchHeader, + argTypes: { + resetted: { action: 'resetted' }, + }, + args: { + header: 'My title', + }, +} diff --git a/libs/angular-accelerator/src/lib/directives/if-permission.directive.ts b/libs/angular-accelerator/src/lib/directives/if-permission.directive.ts index ed98fbc6..82023219 100644 --- a/libs/angular-accelerator/src/lib/directives/if-permission.directive.ts +++ b/libs/angular-accelerator/src/lib/directives/if-permission.directive.ts @@ -29,8 +29,8 @@ export const HAS_PERMISSION_CHECKER = new InjectionToken(' @Directive({ selector: '[ocxIfPermission], [ocxIfNotPermission]' }) export class IfPermissionDirective implements OnInit { - @Input('ocxIfPermission') permission: string | undefined - @Input('ocxIfNotPermission') set notPermission(value: string | undefined) { + @Input('ocxIfPermission') permission: string | string[] | undefined + @Input('ocxIfNotPermission') set notPermission(value: string | string[] | undefined) { this.permission = value this.negate = true } @@ -43,6 +43,13 @@ export class IfPermissionDirective implements OnInit { this.ocxIfPermissionPermissions = value } + @Input() + ocxIfPermissionElseTemplate: TemplateRef | undefined + @Input() + set ocxIfNotPermissionElseTemplate(value: TemplateRef | undefined) { + this.ocxIfPermissionElseTemplate = value + } + private permissionChecker: HasPermissionChecker | undefined negate = false @@ -65,11 +72,15 @@ export class IfPermissionDirective implements OnInit { ngOnInit() { if (this.permission) { - if (this.negate === this.hasPermission(this.permission)) { - if (this.onMissingPermission === 'disable') { - this.renderer.setAttribute(this.el.nativeElement, 'disabled', 'disabled') + if (this.negate === this.hasPermission(Array.isArray(this.permission) ? this.permission : [this.permission])) { + if (this.ocxIfPermissionElseTemplate) { + this.viewContainer.createEmbeddedView(this.ocxIfPermissionElseTemplate) } else { - this.viewContainer.clear() + if (this.onMissingPermission === 'disable') { + this.renderer.setAttribute(this.el.nativeElement, 'disabled', 'disabled') + } else { + this.viewContainer.clear() + } } } else { if (this.templateRef) { @@ -79,14 +90,14 @@ export class IfPermissionDirective implements OnInit { } } - hasPermission(permission: string) { + hasPermission(permission: string[]) { if (this.ocxIfPermissionPermissions) { - const result = this.ocxIfPermissionPermissions.includes(permission) + const result = permission.every((p) => this.ocxIfPermissionPermissions?.includes(p)) if (!result) { console.log('👮‍♀️ No permission in overwrites for: `', permission) } return result } - return this.permissionChecker?.hasPermission(permission) + return permission.every((p) => this.permissionChecker?.hasPermission(p)) } } diff --git a/libs/angular-accelerator/src/lib/model/search-config-info.ts b/libs/angular-accelerator/src/lib/model/search-config-info.ts deleted file mode 100644 index 693a6522..00000000 --- a/libs/angular-accelerator/src/lib/model/search-config-info.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface SearchConfigInfo { - id: string - name: string -} diff --git a/libs/angular-accelerator/src/lib/utils/create-remote-component-and-mfe-translate-loader.utils.ts b/libs/angular-accelerator/src/lib/utils/create-remote-component-and-mfe-translate-loader.utils.ts new file mode 100644 index 00000000..2465e6ed --- /dev/null +++ b/libs/angular-accelerator/src/lib/utils/create-remote-component-and-mfe-translate-loader.utils.ts @@ -0,0 +1,32 @@ +import { HttpClient } from '@angular/common/http' +import { ReplaySubject, map } from 'rxjs' +import { TranslationCacheService } from '../services/translation-cache.service' +import { AppStateService } from '@onecx/angular-integration-interface' +import { AsyncTranslateLoader } from './async-translate-loader.utils' +import { TranslateCombinedLoader } from './translate.combined.loader' +import { createRemoteComponentTranslateLoader } from './create-remote-component-translate-loader.utils' +import { CachingTranslateLoader } from './caching-translate-loader.utils' +import { Location } from '@angular/common' + +export function createRemoteComponentAndMfeTranslateLoader( + httpClient: HttpClient, + baseUrl: ReplaySubject, + translationCacheService: TranslationCacheService, + appStateService: AppStateService +) { + return new AsyncTranslateLoader( + appStateService.currentMfe$.pipe( + map((currentMfe) => { + return new TranslateCombinedLoader( + createRemoteComponentTranslateLoader(httpClient, baseUrl, translationCacheService), + new CachingTranslateLoader( + translationCacheService, + httpClient, + Location.joinWithSlash(currentMfe.remoteBaseUrl, 'assets/i18n/'), + '.json' + ) + ) + }) + ) + ) +} diff --git a/libs/angular-accelerator/testing/index.ts b/libs/angular-accelerator/testing/index.ts index 65912d3e..03f238b6 100644 --- a/libs/angular-accelerator/testing/index.ts +++ b/libs/angular-accelerator/testing/index.ts @@ -11,7 +11,7 @@ export * from './group-by-count-diagram.harness' export * from './interactive-data-view.harness' export * from './more-actions-menu-button.harness' export * from './page-header.harness' -export * from './search-config.harness' +export * from './slot.harness' export * from './search-header.harness' export * from '@angular/cdk/testing' diff --git a/libs/angular-accelerator/testing/interactive-data-view.harness.ts b/libs/angular-accelerator/testing/interactive-data-view.harness.ts index f17cce6e..af0cdff5 100644 --- a/libs/angular-accelerator/testing/interactive-data-view.harness.ts +++ b/libs/angular-accelerator/testing/interactive-data-view.harness.ts @@ -4,6 +4,7 @@ import { PDropdownHarness } from '@onecx/angular-testing' import { CustomGroupColumnSelectorHarness } from '.' import { DataLayoutSelectionHarness } from './data-layout-selection.harness' import { DataViewHarness } from './data-view.harness' +import { SlotHarness } from './slot.harness' export class InteractiveDataViewHarness extends ContentContainerComponentHarness { static hostSelector = 'ocx-interactive-data-view' @@ -12,7 +13,8 @@ export class InteractiveDataViewHarness extends ContentContainerComponentHarness getColumnGroupSelectionDropdown = this.locatorForOptional( PDropdownHarness.with({ id: 'columnGroupSelectionDropdown' }) ) - getCustomGroupColumnSelector = this.locatorFor(CustomGroupColumnSelectorHarness) + getCustomGroupColumnSelector = this.locatorForOptional(CustomGroupColumnSelectorHarness) + getCustomGroupColumnSelectorSlot = this.locatorForOptional(SlotHarness) getDataListGridSortingDropdown = this.locatorForOptional(PDropdownHarness.with({ id: 'dataListGridSortingDropdown' })) getDataListGridSortingButton = this.locatorForOptional(PButtonHarness.with({ id: 'dataListGridSortingButton' })) getDataView = this.locatorFor(DataViewHarness) diff --git a/libs/angular-accelerator/testing/search-config.harness.ts b/libs/angular-accelerator/testing/search-config.harness.ts deleted file mode 100644 index 1820f6cd..00000000 --- a/libs/angular-accelerator/testing/search-config.harness.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ContentContainerComponentHarness } from '@angular/cdk/testing' -import { PDropdownHarness } from '@onecx/angular-testing' - -export class SearchConfigHarness extends ContentContainerComponentHarness { - static hostSelector = 'ocx-search-config' - - getSearchConfigDropdown = this.locatorForOptional(PDropdownHarness.with({ id: 'searchConfig' })) -} diff --git a/libs/angular-accelerator/testing/search-header.harness.ts b/libs/angular-accelerator/testing/search-header.harness.ts index 941634ad..90986e08 100644 --- a/libs/angular-accelerator/testing/search-header.harness.ts +++ b/libs/angular-accelerator/testing/search-header.harness.ts @@ -1,14 +1,12 @@ import { ComponentHarness } from '@angular/cdk/testing' import { PButtonHarness } from '@onecx/angular-testing' import { PageHeaderHarness } from './page-header.harness' -import { SearchConfigHarness } from './search-config.harness' import { MoreActionsMenuButtonHarness } from './more-actions-menu-button.harness' export class SearchHeaderHarness extends ComponentHarness { static hostSelector = 'ocx-search-header' getPageHeader = this.locatorFor(PageHeaderHarness) - getSearchConfig = this.locatorFor(SearchConfigHarness) getSearchButton = this.locatorFor( PButtonHarness.with({ id: 'searchButton', diff --git a/libs/angular-accelerator/testing/slot.harness.ts b/libs/angular-accelerator/testing/slot.harness.ts new file mode 100644 index 00000000..f6083f4c --- /dev/null +++ b/libs/angular-accelerator/testing/slot.harness.ts @@ -0,0 +1,9 @@ +import { BaseHarnessFilters, ContentContainerComponentHarness } from '@angular/cdk/testing' + +export interface SlotHarnessFilters extends BaseHarnessFilters { + name?: string +} + +export class SlotHarness extends ContentContainerComponentHarness { + static hostSelector = 'ocx-slot' +} diff --git a/libs/angular-integration-interface/mocks/index.ts b/libs/angular-integration-interface/mocks/index.ts index 1f89a426..8b444f7b 100644 --- a/libs/angular-integration-interface/mocks/index.ts +++ b/libs/angular-integration-interface/mocks/index.ts @@ -1,2 +1,3 @@ export * from './mock-user-service' export * from './app-state-service-mock' +export * from './fake-topic' diff --git a/libs/angular-integration-interface/src/index.ts b/libs/angular-integration-interface/src/index.ts index ffd738f4..942aee7c 100644 --- a/libs/angular-integration-interface/src/index.ts +++ b/libs/angular-integration-interface/src/index.ts @@ -1,4 +1,5 @@ // services +export * from './lib/services/app-config-service' export * from './lib/services/app-state.service' export * from './lib/services/configuration.service' export * from './lib/services/user.service' diff --git a/libs/angular-accelerator/src/lib/services/app-config-service.ts b/libs/angular-integration-interface/src/lib/services/app-config-service.ts similarity index 100% rename from libs/angular-accelerator/src/lib/services/app-config-service.ts rename to libs/angular-integration-interface/src/lib/services/app-config-service.ts diff --git a/libs/angular-remote-components/mocks/index.ts b/libs/angular-remote-components/mocks/index.ts new file mode 100644 index 00000000..085a3d9f --- /dev/null +++ b/libs/angular-remote-components/mocks/index.ts @@ -0,0 +1 @@ +export * from './slot-service-mock' diff --git a/libs/angular-remote-components/mocks/ng-package.json b/libs/angular-remote-components/mocks/ng-package.json new file mode 100644 index 00000000..ecef3ed8 --- /dev/null +++ b/libs/angular-remote-components/mocks/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/libs/angular-remote-components/mocks/slot-service-mock.ts b/libs/angular-remote-components/mocks/slot-service-mock.ts new file mode 100644 index 00000000..a64dfa4e --- /dev/null +++ b/libs/angular-remote-components/mocks/slot-service-mock.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core' +import { BehaviorSubject, Observable, map } from 'rxjs' + +@Injectable() +export class SlotServiceMock { + _componentsDefinedForSlot: BehaviorSubject<{ + [slot_key: string]: string[] + }> = new BehaviorSubject({}) + isSomeComponentDefinedForSlot(slotName: string): Observable { + return this._componentsDefinedForSlot.pipe( + map((assignments) => { + return slotName in assignments && assignments[slotName].length > 0 + }) + ) + } + + getComponentsForSlot(slotName: string) { + return this._componentsDefinedForSlot.pipe( + map((assignments) => { + return Object.keys(assignments).includes(slotName) ? assignments[slotName] : [] + }) + ) + } + + assignComponentToSlot(componentName: string, slotName: string) { + const currentAssignments = this._componentsDefinedForSlot.getValue() + this._componentsDefinedForSlot.next({ + ...currentAssignments, + [slotName]: slotName in currentAssignments ? currentAssignments[slotName].concat(componentName) : [componentName], + }) + } +} diff --git a/libs/angular-remote-components/package.json b/libs/angular-remote-components/package.json index 9c5c2d0f..71fe4191 100644 --- a/libs/angular-remote-components/package.json +++ b/libs/angular-remote-components/package.json @@ -5,7 +5,6 @@ "peerDependencies": { "@angular/common": "^18.0.5", "@angular/core": "^18.0.5", - "@onecx/angular-accelerator": "^5", "@onecx/integration-interface": "^5", "@ngx-translate/core": "^15.0.0", "@angular-architects/module-federation": "^18.0.4", diff --git a/libs/angular-remote-components/src/lib/angular-remote-components.module.ts b/libs/angular-remote-components/src/lib/angular-remote-components.module.ts index 43a2f528..5f03ca09 100644 --- a/libs/angular-remote-components/src/lib/angular-remote-components.module.ts +++ b/libs/angular-remote-components/src/lib/angular-remote-components.module.ts @@ -1,12 +1,17 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { SlotComponent } from './components/slot/slot.component' -import { AppConfigService } from '@onecx/angular-accelerator' +import { SLOT_SERVICE, SlotService } from './services/slot.service' @NgModule({ imports: [CommonModule], declarations: [SlotComponent], exports: [SlotComponent], - providers: [AppConfigService], + providers: [ + { + provide: SLOT_SERVICE, + useExisting: SlotService, + }, + ], }) export class AngularRemoteComponentsModule {} diff --git a/libs/angular-remote-components/src/lib/components/slot/slot.component.ts b/libs/angular-remote-components/src/lib/components/slot/slot.component.ts index 0fd370b1..5dd7378d 100644 --- a/libs/angular-remote-components/src/lib/components/slot/slot.component.ts +++ b/libs/angular-remote-components/src/lib/components/slot/slot.component.ts @@ -7,6 +7,7 @@ import { Input, OnDestroy, OnInit, + Optional, QueryList, TemplateRef, Type, @@ -118,8 +119,6 @@ export class SlotComponent implements OnInit, OnDestroy { }) } - updateDataSub: Subscription | undefined - _viewContainers$ = new BehaviorSubject | undefined>(undefined) @ViewChildren('slot', { read: ViewContainerRef }) set viewContainers(value: QueryList) { @@ -131,9 +130,13 @@ export class SlotComponent implements OnInit, OnDestroy { subscription: Subscription | undefined components$: Observable | undefined - constructor(@Inject(SLOT_SERVICE) private slotService: SlotService) {} + constructor(@Optional() @Inject(SLOT_SERVICE) private slotService?: SlotService) {} ngOnInit(): void { + if (!this.slotService) { + console.error(`SLOT_SERVICE token was not provided. ${this.name} slot will not be filled with data.`) + return + } this.components$ = this.slotService.getComponentsForSlot(this.name) combineLatest([this._assignedComponents$, this._inputs$, this._outputs$]).subscribe( ([components, inputs, outputs]) => { diff --git a/libs/angular-remote-components/src/lib/services/slot.service.ts b/libs/angular-remote-components/src/lib/services/slot.service.ts index 4e1b84da..0ef5abb4 100644 --- a/libs/angular-remote-components/src/lib/services/slot.service.ts +++ b/libs/angular-remote-components/src/lib/services/slot.service.ts @@ -59,7 +59,11 @@ export class SlotService implements SlotServiceInterface { isSomeComponentDefinedForSlot(slotName: string): Observable { return this.remoteComponents$.pipe( - map((remoteComponentsInfo) => remoteComponentsInfo.slots.some((slotMapping) => slotMapping.name === slotName)) + map((remoteComponentsInfo) => + remoteComponentsInfo.slots.some( + (slotMapping) => slotMapping.name === slotName && slotMapping.components.length > 0 + ) + ) ) } diff --git a/libs/angular-testing/src/lib/harnesses/primeng/p-dropdown.harness.ts b/libs/angular-testing/src/lib/harnesses/primeng/p-dropdown.harness.ts index f6ca0a26..bfd9734a 100644 --- a/libs/angular-testing/src/lib/harnesses/primeng/p-dropdown.harness.ts +++ b/libs/angular-testing/src/lib/harnesses/primeng/p-dropdown.harness.ts @@ -21,6 +21,10 @@ export class PDropdownHarness extends ContentContainerComponentHarness { return await (await this.host()).getAttribute('inputId') } + async getAriaLabel(): Promise { + return (await this.locatorForOptional('span.p-placeholder')())?.getAttribute('aria-label') + } + async getId(): Promise { return await (await this.host()).getAttribute('id') } @@ -29,6 +33,10 @@ export class PDropdownHarness extends ContentContainerComponentHarness { return (await this.locatorForOptional('span.p-placeholder')())?.text() } + async getSelectedText() { + return (await this.locatorForOptional('span.p-dropdown-label')())?.text() + } + async isOpen(): Promise { return (await this.locatorFor('div')()).hasClass('p-dropdown-open') } diff --git a/libs/portal-integration-angular/src/index.ts b/libs/portal-integration-angular/src/index.ts index 320d9c0c..c8c09b7c 100644 --- a/libs/portal-integration-angular/src/index.ts +++ b/libs/portal-integration-angular/src/index.ts @@ -39,7 +39,6 @@ export * from './lib/core/components/button-dialog/dialog-message-content/dialog export * from './lib/core/components/loading-indicator/loading-indicator.component' export * from './lib/core/components/content-container/content-container.component' export * from './lib/core/components/content/content.component' -export * from './lib/core/components/create-or-edit-search-config-dialog/create-or-edit-search-config-dialog.component' export * from './lib/core/components/lifecycle/lifecycle.component' // services @@ -97,5 +96,5 @@ export { APP_CONFIG, AUTH_SERVICE, SANITY_CHECK, - APPLICATION_NAME + APPLICATION_NAME, } from '@onecx/angular-integration-interface' diff --git a/libs/portal-integration-angular/src/lib/core/components/create-or-edit-search-config-dialog/create-or-edit-search-config-dialog.component.html b/libs/portal-integration-angular/src/lib/core/components/create-or-edit-search-config-dialog/create-or-edit-search-config-dialog.component.html deleted file mode 100644 index 182dc5f7..00000000 --- a/libs/portal-integration-angular/src/lib/core/components/create-or-edit-search-config-dialog/create-or-edit-search-config-dialog.component.html +++ /dev/null @@ -1,26 +0,0 @@ -
-
- -
- -
- - - -
- -
- - -
-
diff --git a/libs/portal-integration-angular/src/lib/core/components/create-or-edit-search-config-dialog/create-or-edit-search-config-dialog.component.scss b/libs/portal-integration-angular/src/lib/core/components/create-or-edit-search-config-dialog/create-or-edit-search-config-dialog.component.scss deleted file mode 100644 index a530b42b..00000000 --- a/libs/portal-integration-angular/src/lib/core/components/create-or-edit-search-config-dialog/create-or-edit-search-config-dialog.component.scss +++ /dev/null @@ -1,14 +0,0 @@ -.searchConfigDialog { - display: flex; - flex-direction: column; - gap: 1em; - margin-bottom: 1em; -} - -:host ::ng-deep .p-inputtext { - width: 100%; -} - -:host ::ng-deep .p-checkbox { - margin-right: 1em; -} diff --git a/libs/portal-integration-angular/src/lib/core/components/create-or-edit-search-config-dialog/create-or-edit-search-config-dialog.component.spec.ts b/libs/portal-integration-angular/src/lib/core/components/create-or-edit-search-config-dialog/create-or-edit-search-config-dialog.component.spec.ts deleted file mode 100644 index 90e4937b..00000000 --- a/libs/portal-integration-angular/src/lib/core/components/create-or-edit-search-config-dialog/create-or-edit-search-config-dialog.component.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing' -import { CreateOrEditSearchConfigDialogComponent } from './create-or-edit-search-config-dialog.component' -import { CheckboxModule } from 'primeng/checkbox' -import { MockAuthModule } from '../../../mock-auth/mock-auth.module' -import { TranslateTestingModule } from 'ngx-translate-testing' -import { provideHttpClientTesting } from '@angular/common/http/testing' -import { TranslateService } from '@ngx-translate/core' -import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed' -import { CreateOrEditSearchConfigDialogHarness } from '../../../../../testing' -import { PCheckboxHarness } from '@onecx/angular-testing' -import { DialogState } from '../../../services/portal-dialog.service' -import { ReactiveFormsModule } from '@angular/forms' -import { InputTextModule } from 'primeng/inputtext' -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; - -describe('CreateOrEditSearchConfigDialogComponent', () => { - let component: CreateOrEditSearchConfigDialogComponent - let fixture: ComponentFixture - let translateService: TranslateService - let dialogHarness: CreateOrEditSearchConfigDialogHarness - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [CreateOrEditSearchConfigDialogComponent], - imports: [CheckboxModule, - MockAuthModule, - TranslateTestingModule.withTranslations({ - en: require('./../../../../../assets/i18n/en.json'), - de: require('./../../../../../assets/i18n/de.json'), - }), - ReactiveFormsModule, - InputTextModule], - providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] -}).compileComponents() - - fixture = TestBed.createComponent(CreateOrEditSearchConfigDialogComponent) - component = fixture.componentInstance - - translateService = TestBed.inject(TranslateService) - translateService.setDefaultLang('en') - translateService.use('en') - - fixture.detectChanges() - dialogHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, CreateOrEditSearchConfigDialogHarness) - }) - - it('should create the component', () => { - expect(component).toBeTruthy() - }) - - it('should load the CreateOrEditSearchConfigDialogHarness', async () => { - expect(dialogHarness).toBeTruthy() - }) - - it('should set the DialogResult of the saveInputValuesId checkbox to true when the saveInputValuesId checkbox is checked', async () => { - const saveInputValuesCheckbox = await dialogHarness.getHarness( - PCheckboxHarness.with({ inputid: 'saveInputValuesId' }) - ) - await saveInputValuesCheckbox.click() - const _state: DialogState = { button: 'primary', result: undefined } - component.ocxDialogButtonClicked(_state) - const dialogResult = { - searchConfigName: '', - saveInputValues: true, - saveColumns: false, - } - expect(component.dialogResult).toEqual(dialogResult) - }) - - it('should set the DialogResult of the saveColumnsId checkbox initially false', async () => { - const _state: DialogState = { button: 'primary', result: undefined } - await component.ocxDialogButtonClicked(_state) - const dialogResult = { - searchConfigName: '', - saveInputValues: false, - saveColumns: false, - } - expect(component.dialogResult).toEqual(dialogResult) - }) - - it('should set the DialogResult of the searchConfig input Field to the entered value', async () => { - await (await dialogHarness.getSearchConfigInputHarness()).setValue('search Config') - const _state: DialogState = { button: 'primary', result: undefined } - await component.ocxDialogButtonClicked(_state) - const dialogResult = { - searchConfigName: 'search Config', - saveInputValues: false, - saveColumns: false, - } - expect(component.dialogResult).toEqual(dialogResult) - }) - - it('should set the saveColumnsId checkbox initially to unchecked', async () => { - const saveInputValuesCheckbox = await dialogHarness.getSaveColumnsCheckboxHarness() - const checked = await saveInputValuesCheckbox.isChecked() - expect(checked).toBeFalsy() - }) - - it('should set the saveInputValues checkbox initially to unchecked', async () => { - const saveInputValuesCheckbox = await dialogHarness.getSaveInputValuesCheckboxHarness() - const checked = await saveInputValuesCheckbox.isChecked() - expect(checked).toBeFalsy() - }) - - it('should set the saveInputValues checkbox to true when it is clicked', async () => { - const saveInputValuesCheckbox = await dialogHarness.getSaveInputValuesCheckboxHarness() - await saveInputValuesCheckbox.click() - const checked = await saveInputValuesCheckbox.isChecked() - expect(checked).toBeTruthy() - }) - - it('should emit true when the searchConfig name is not an empty string and the saveColumnsCheckBox is clicked', async () => { - let done: () => void - const finished = new Promise((resolve) => (done = resolve)) - let enabled = false - component.primaryButtonEnabled.subscribe((v) => { - enabled = v - done() - }) - - const searchConfigInputHarness = await dialogHarness.getSearchConfigInputHarness() - searchConfigInputHarness.setValue('test') - const saveInputValuesCheckbox = await dialogHarness.getSaveColumnsCheckboxHarness() - await saveInputValuesCheckbox.click() - - await finished - expect(enabled).toEqual(true) - }) - - it('emit true when the searchConfig Name is not an empty string and the saveInputValuesCheckbox is clicked', async () => { - let done: () => void - const finished = new Promise((resolve) => (done = resolve)) - let enabled = false - component.primaryButtonEnabled.subscribe((v) => { - enabled = v - done() - }) - - const searchConfigInputHarness = await dialogHarness.getSearchConfigInputHarness() - searchConfigInputHarness.setValue('test') - const saveInputValuesCheckbox = await dialogHarness.getSaveInputValuesCheckboxHarness() - await saveInputValuesCheckbox.click() - - await finished - expect(enabled).toEqual(true) - }) -}) diff --git a/libs/portal-integration-angular/src/lib/core/components/create-or-edit-search-config-dialog/create-or-edit-search-config-dialog.component.ts b/libs/portal-integration-angular/src/lib/core/components/create-or-edit-search-config-dialog/create-or-edit-search-config-dialog.component.ts deleted file mode 100644 index 3ad73901..00000000 --- a/libs/portal-integration-angular/src/lib/core/components/create-or-edit-search-config-dialog/create-or-edit-search-config-dialog.component.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core' -import { FormControl, FormGroup } from '@angular/forms' -import { - DialogButtonClicked, - DialogPrimaryButtonDisabled, - DialogResult, - DialogState, -} from '../../../services/portal-dialog.service' -import { Observable, map } from 'rxjs' - -export type CreateOrEditSearchDialogContent = { - searchConfigName: string - saveInputValues: boolean - saveColumns: boolean -} -@Component({ - selector: 'ocx-create-or-edit-search-config-dialog', - templateUrl: './create-or-edit-search-config-dialog.component.html', - styleUrls: ['./create-or-edit-search-config-dialog.component.scss'], -}) -export class CreateOrEditSearchConfigDialogComponent - implements - DialogPrimaryButtonDisabled, - DialogResult, - DialogButtonClicked -{ - @Input() - set searchConfigName(value: string | undefined){ - this.searchConfigFormGroup.controls['searchConfigName'].setValue(value) - } - get searchConfigName(): string | undefined { - return this.searchConfigFormGroup.controls['searchConfigName'].value - } - - @Input() - set saveInputValues(value: boolean | undefined) { - this.searchConfigFormGroup.controls['saveInputValues'].setValue(value) - } - get saveInputValues(): boolean | undefined { - return this.searchConfigFormGroup.controls['saveInputValues'].value - } - - @Input() - set saveColumns(value: boolean | undefined) { - this.searchConfigFormGroup.controls['saveColumns'].setValue(value) - } - get saveColumns(): boolean | undefined { - return this.searchConfigFormGroup.controls['saveColumns'].value - } - - @Output() primaryButtonEnabled: EventEmitter = new EventEmitter() - - searchConfigFormGroup: FormGroup = new FormGroup({ - searchConfigName: new FormControl(''), - saveInputValues: new FormControl(false), - saveColumns: new FormControl(false), - }) - placeHolderKey = 'OCX_SEARCH_CONFIG.PLACEHOLDER' - dialogResult: CreateOrEditSearchDialogContent = { searchConfigName: '', saveInputValues: false, saveColumns: false } - - constructor() { - this.searchConfigFormGroup.valueChanges - .pipe( - map( - (dialogFormValues: CreateOrEditSearchDialogContent) => - !!dialogFormValues.searchConfigName && (dialogFormValues.saveInputValues || dialogFormValues.saveColumns) - ) - ) - .subscribe(this.primaryButtonEnabled) - } - - ocxDialogButtonClicked( - _state: DialogState - ): boolean | Observable | Promise | undefined { - this.dialogResult = { - searchConfigName: this.searchConfigFormGroup?.get('searchConfigName')?.value, - saveInputValues: this.searchConfigFormGroup?.get('saveInputValues')?.value, - saveColumns: this.searchConfigFormGroup?.get('saveColumns')?.value, - } - return true - } -} diff --git a/libs/portal-integration-angular/src/lib/core/portal-core.module.ts b/libs/portal-integration-angular/src/lib/core/portal-core.module.ts index e816ec4d..9b71e850 100644 --- a/libs/portal-integration-angular/src/lib/core/portal-core.module.ts +++ b/libs/portal-integration-angular/src/lib/core/portal-core.module.ts @@ -22,7 +22,6 @@ import { ButtonDialogComponent } from './components/button-dialog/button-dialog. import { DialogMessageContentComponent } from './components/button-dialog/dialog-message-content/dialog-message-content.component' import { OcxContentContainerComponent } from './components/content-container/content-container.component' import { OcxContentComponent } from './components/content/content.component' -import { CreateOrEditSearchConfigDialogComponent } from './components/create-or-edit-search-config-dialog/create-or-edit-search-config-dialog.component' import { ColumnTogglerComponent } from './components/data-view-controls/column-toggler-component/column-toggler.component' import { DataViewControlsComponent } from './components/data-view-controls/data-view-controls.component' import { ViewTemplatePickerComponent } from './components/data-view-controls/view-template-picker/view-template-picker.component' @@ -113,7 +112,6 @@ export class PortalMissingTranslationHandler implements MissingTranslationHandle OcxContentContainerDirective, OcxContentComponent, OcxContentContainerComponent, - CreateOrEditSearchConfigDialogComponent, LifecycleComponent, ], providers: [ @@ -169,7 +167,6 @@ export class PortalMissingTranslationHandler implements MissingTranslationHandle OcxContentContainerDirective, OcxContentComponent, OcxContentContainerComponent, - CreateOrEditSearchConfigDialogComponent, LifecycleComponent, ], }) diff --git a/libs/portal-integration-angular/testing/create-or-edit-search-config-dialog.harness.ts b/libs/portal-integration-angular/testing/create-or-edit-search-config-dialog.harness.ts deleted file mode 100644 index 1de86db7..00000000 --- a/libs/portal-integration-angular/testing/create-or-edit-search-config-dialog.harness.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ContentContainerComponentHarness } from '@angular/cdk/testing' -import { PCheckboxHarness, InputHarness } from '@onecx/angular-testing' - -export class CreateOrEditSearchConfigDialogHarness extends ContentContainerComponentHarness { - static hostSelector = 'ocx-create-or-edit-search-config-dialog' - - getSaveInputValuesCheckboxHarness() { - return this.getHarness(PCheckboxHarness.with({ inputid: 'saveInputValuesId' })) - } - - getSaveColumnsCheckboxHarness() { - return this.getHarness(PCheckboxHarness.with({ inputid: 'saveColumnsId' })) - } - - getSearchConfigInputHarness() { - return this.getHarness(InputHarness.with({ id: 'searchConfigName' })) - } -} diff --git a/libs/portal-integration-angular/testing/index.ts b/libs/portal-integration-angular/testing/index.ts index 5fcca82e..bd7d1fdd 100644 --- a/libs/portal-integration-angular/testing/index.ts +++ b/libs/portal-integration-angular/testing/index.ts @@ -1,7 +1,6 @@ export * from './button-dialog.harness' export * from './content-container.harness' export * from './content.harness' -export * from './create-or-edit-search-config-dialog.harness' export * from './dialog-message-content.harness' export * from './lifecycle.harness' diff --git a/tsconfig.base.json b/tsconfig.base.json index fc6d9f85..8fb61513 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -22,6 +22,7 @@ "@onecx/angular-integration-interface": ["libs/angular-integration-interface/src/index.ts"], "@onecx/angular-integration-interface/mocks": ["libs/angular-integration-interface/mocks/index.ts"], "@onecx/angular-remote-components": ["libs/angular-remote-components/src/index.ts"], + "@onecx/angular-remote-components/mocks": ["libs/angular-remote-components/mocks/index.ts"], "@onecx/angular-standalone-shell": ["libs/angular-standalone-shell/src/index.ts"], "@onecx/angular-testing": ["libs/angular-testing/src/index.ts"], "@onecx/angular-webcomponents": ["libs/angular-webcomponents/src/index.ts"],