From da4bce8bac4e1d51063b4803552f5509ba30d76a Mon Sep 17 00:00:00 2001 From: Jalinson Diaz Date: Thu, 7 Sep 2023 10:53:32 -0300 Subject: [PATCH] Fix #25805: Containers Portlet only show containers from current site (#25966) * refactor (containers portlet): table is showing and partially working * fix (containers list module): querying and pagination were broken * fix (containers list styles): table was overflowing * dev (container list module): add missing functionalities * copywritting (container list component): fix comment * fix (container list tests): fixed some tests that were broken * dev (container list test): add and fix tests * copywirtting (container list test): delete unnecessary comment * fix (dot containers store): portlet not initializing on navigation * fix (dot containers store): pagination service is not cleaning extra params on navigation * dev (containers list store): small refactor to getbyHost * fix (containers list test): fix broken tests --- .../container-list.component.html | 181 +++++++----- .../container-list.component.scss | 37 ++- .../container-list.component.spec.ts | 271 +++++++++++------- .../container-list.component.ts | 119 +++++--- .../container-list/container-list.module.ts | 10 +- .../store/dot-container-list.store.ts | 229 +++++++++++++-- .../dot-content-type-selector.component.html | 1 + .../lib/paginator/paginator.service.spec.ts | 9 + .../src/lib/paginator/paginator.service.ts | 9 + 9 files changed, 614 insertions(+), 252 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.html index ae87ccb6a17a..a4e8aa38d4bb 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.html @@ -1,63 +1,96 @@ -
- - -
-
- - -
- -
- - - -
+ + + + +
+
+
- - - +
+ - - + +
+
+
+ + + + + + + + {{ col.header }} + + + + + + + + + + + {{ rowData.name }} - @@ -66,18 +99,17 @@ }} - + + size="14px"> - + {{ rowData.friendlyName }} - + {{ rowData.modDate | dotRelativeDate }} @@ -86,17 +118,20 @@ *ngIf="!rowData.disableInteraction" [attr.data-testid]="rowData.identifier" [actions]="setContainerActions(rowData)" - [item]="rowData" - > + [item]="rowData"> - - + + + +
+ {{ 'No-Results-Found' | dm }} +
+
+
- -
-
+ +
diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.scss b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.scss index 8c421494aea8..e04b256db9fb 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.scss +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.scss @@ -1,5 +1,30 @@ @use "variables" as *; +:host { + height: 100%; + overflow-y: auto; + + ::ng-deep { + dot-portlet-box { + display: flex; + flex-direction: column; + overflow-y: auto; + } + + p-table { + flex-grow: 1; + } + + .p-datatable, + .p-table { + display: flex; + flex-direction: column; + height: 100%; + justify-content: space-between; + background: $white; + } + } +} .container-listing__header-options { width: 100%; display: flex; @@ -13,11 +38,6 @@ } } -.container-listing ::ng-deep { - overflow-y: auto; - height: 100%; -} - .container-listing__path { color: $color-palette-gray-500; } @@ -25,3 +45,10 @@ dot-content-type-selector { margin-right: $spacing-3; } + +.listing-datatable__empty { + display: flex; + justify-content: center; + font-size: $font-size-xxl; + margin-top: $spacing-4; +} diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts index cc445c35e025..c3d2aabb4a11 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.spec.ts @@ -1,32 +1,35 @@ import { of } from 'rxjs'; import { CommonModule } from '@angular/common'; +import { HttpClient } from '@angular/common/http'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { Component, CUSTOM_ELEMENTS_SCHEMA, EventEmitter, Input, Output } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA, Component, EventEmitter, Input, Output } from '@angular/core'; import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ActivatedRoute } from '@angular/router'; -import { ConfirmationService, SelectItem, SharedModule } from 'primeng/api'; +import { ConfirmationService, SelectItem } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; import { CheckboxModule } from 'primeng/checkbox'; -import { DialogService, DynamicDialogModule } from 'primeng/dynamicdialog'; +import { DialogService } from 'primeng/dynamicdialog'; +import { InputTextModule } from 'primeng/inputtext'; import { Menu, MenuModule } from 'primeng/menu'; +import { Table, TableModule } from 'primeng/table'; -import { DotActionButtonModule } from '@components/_common/dot-action-button/dot-action-button.module'; import { DotActionMenuButtonComponent } from '@components/_common/dot-action-menu-button/dot-action-menu-button.component'; import { DotActionMenuButtonModule } from '@components/_common/dot-action-menu-button/dot-action-menu-button.module'; import { DotAddToBundleModule } from '@components/_common/dot-add-to-bundle'; -import { DotListingDataTableModule } from '@components/dot-listing-data-table'; -import { DotListingDataTableComponent } from '@components/dot-listing-data-table/dot-listing-data-table.component'; +import { DotEmptyStateModule } from '@components/_common/dot-empty-state/dot-empty-state.module'; +import { ActionHeaderModule } from '@components/dot-listing-data-table/action-header/action-header.module'; import { DotMessageDisplayServiceMock } from '@components/dot-message-display/dot-message-display.component.spec'; import { DotMessageDisplayService } from '@components/dot-message-display/services'; +import { DotPortletBaseModule } from '@components/dot-portlet-base/dot-portlet-base.module'; import { DotRelativeDatePipe } from '@dotcms/app/view/pipes/dot-relative-date/dot-relative-date.pipe'; import { DotAlertConfirmService, DotMessageService, - DotSiteBrowserService + DotSiteBrowserService, + PaginatorService } from '@dotcms/data-access'; import { CoreWebService, @@ -36,19 +39,29 @@ import { DotEventsSocket, DotEventsSocketURL, DotPushPublishDialogService, + LoggerService, LoginService, + mockSites, + SiteService, StringUtils } from '@dotcms/dotcms-js'; import { CONTAINER_SOURCE, DotActionBulkResult, DotContainer } from '@dotcms/dotcms-models'; import { DotMessagePipe } from '@dotcms/ui'; -import { DotFormatDateServiceMock, MockDotMessageService } from '@dotcms/utils-testing'; +import { + DotcmsConfigServiceMock, + DotFormatDateServiceMock, + MockDotMessageService, + SiteServiceMock +} from '@dotcms/utils-testing'; import { DotContainersService } from '@services/dot-containers/dot-containers.service'; import { DotFormatDateService } from '@services/dot-format-date-service'; import { DotHttpErrorManagerService } from '@services/dot-http-error-manager/dot-http-error-manager.service'; import { DotRouterService } from '@services/dot-router/dot-router.service'; import { dotEventSocketURLFactory } from '@tests/dot-test-bed'; +import { ContainerListRoutingModule } from './container-list-routing.module'; import { ContainerListComponent } from './container-list.component'; +import { DotContainerListStore } from './store/dot-container-list.store'; const containersMock: DotContainer[] = [ { @@ -138,29 +151,6 @@ const containersMock: DotContainer[] = [ } ]; -const columnsMock = [ - { - fieldName: 'title', - header: 'Name', - sortable: true - }, - { - fieldName: 'status', - header: 'Status', - width: '8%' - }, - { - fieldName: 'friendlyName', - header: 'Description' - }, - { - fieldName: 'modDate', - format: 'date', - header: 'Last Edit', - sortable: true - } -]; - const messages = { 'Add-To-Bundle': 'Add To Bundle', 'Remote-Publish': 'Push Publish', @@ -222,10 +212,10 @@ class MockDotContentTypeSelectorComponent { describe('ContainerListComponent', () => { let fixture: ComponentFixture; + let table: Table; let comp: ContainerListComponent; - let dotListingDataTable: DotListingDataTableComponent; let dotPushPublishDialogService: DotPushPublishDialogService; - let coreWebService: CoreWebService; + let dotRouterService: DotRouterService; let unPublishContainer: DotActionMenuButtonComponent; @@ -234,6 +224,9 @@ describe('ContainerListComponent', () => { let contentTypesSelector: MockDotContentTypeSelectorComponent; let dotContainersService: DotContainersService; let dotSiteBrowserService: DotSiteBrowserService; + let siteService: SiteServiceMock; + let store: DotContainerListStore; + let paginatorService: PaginatorService; const messageServiceMock = new MockDotMessageService(messages); @@ -241,13 +234,33 @@ describe('ContainerListComponent', () => { await TestBed.configureTestingModule({ declarations: [ContainerListComponent, MockDotContentTypeSelectorComponent], providers: [ - { provide: DotMessageService, useValue: messageServiceMock }, + ConfirmationService, + DialogService, + DotAlertConfirmService, + DotcmsConfigService, + DotcmsEventsService, + DotContainerListStore, + DotContainersService, + DotEventsSocket, + DotHttpErrorManagerService, + DotSiteBrowserService, + HttpClient, + LoggerService, + LoginService, + PaginatorService, + StringUtils, + { + provide: SiteService, + useClass: SiteServiceMock + }, { provide: ActivatedRoute, useClass: ActivatedRouteMock }, - { provide: CoreWebService, useClass: CoreWebServiceMock }, - { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, + { + provide: DotcmsConfigService, + useClass: DotcmsConfigServiceMock + }, { provide: DotRouterService, useValue: { @@ -256,74 +269,61 @@ describe('ContainerListComponent', () => { goToSiteBrowser: jasmine.createSpy() } }, - StringUtils, - DotHttpErrorManagerService, - DotAlertConfirmService, - ConfirmationService, - LoginService, - DotcmsEventsService, - DotEventsSocket, - DotcmsConfigService, - { provide: DotMessageDisplayService, useClass: DotMessageDisplayServiceMock }, - DialogService, - DotSiteBrowserService, - DotContainersService, - { provide: DotFormatDateService, useClass: DotFormatDateServiceMock } + { provide: CoreWebService, useClass: CoreWebServiceMock }, + { provide: DotMessageService, useValue: messageServiceMock }, + { provide: DotEventsSocketURL, useFactory: dotEventSocketURLFactory }, + { provide: DotFormatDateService, useClass: DotFormatDateServiceMock }, + { provide: DotMessageDisplayService, useClass: DotMessageDisplayServiceMock } ], imports: [ - DotListingDataTableModule, - CommonModule, - DotMessagePipe, - SharedModule, - CheckboxModule, - MenuModule, + ActionHeaderModule, ButtonModule, - DotActionButtonModule, + CheckboxModule, + CommonModule, + ContainerListRoutingModule, DotActionMenuButtonModule, DotAddToBundleModule, + DotEmptyStateModule, + DotMessagePipe, + DotPortletBaseModule, DotRelativeDatePipe, HttpClientTestingModule, - DynamicDialogModule, - BrowserAnimationsModule + InputTextModule, + MenuModule, + TableModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents(); - fixture = TestBed.createComponent(ContainerListComponent); - comp = fixture.componentInstance; + dotPushPublishDialogService = TestBed.inject(DotPushPublishDialogService); - coreWebService = TestBed.inject(CoreWebService); dotRouterService = TestBed.inject(DotRouterService); dotContainersService = TestBed.inject(DotContainersService); dotSiteBrowserService = TestBed.inject(DotSiteBrowserService); + siteService = TestBed.inject(SiteService) as unknown as SiteServiceMock; + paginatorService = TestBed.inject(PaginatorService); + spyOn(paginatorService, 'get').and.returnValue(of(containersMock)); + + fixture = TestBed.createComponent(ContainerListComponent); + comp = fixture.componentInstance; + store = fixture.debugElement.injector.get(DotContainerListStore); // To get store instance from the isolated provider }); describe('with data', () => { beforeEach(fakeAsync(() => { - spyOn(coreWebService, 'requestView').and.returnValue( - of({ - entity: containersMock, - header: (type) => (type === 'Link' ? 'test;test=test' : '10') - }) - ); + siteService.setFakeCurrentSite(); fixture.detectChanges(); tick(2); fixture.detectChanges(); - dotListingDataTable = fixture.debugElement.query( - By.css('dot-listing-data-table') - ).componentInstance; + spyOn(dotPushPublishDialogService, 'open'); + table = fixture.debugElement.query( + By.css('[data-testId="container-list-table"]') + ).componentInstance; })); - it('should set attributes of dotListingDataTable', () => { - expect(dotListingDataTable.columns).toEqual(columnsMock); - expect(dotListingDataTable.url).toEqual('v1/containers'); - expect(dotListingDataTable.actions).toEqual([]); - expect(dotListingDataTable.checkbox).toEqual(true); - expect(dotListingDataTable.dataKey).toEqual('inode'); - }); - it('should clicked on row and emit dotRouterService', () => { - comp.listing.dataTable.tableViewChild.nativeElement.rows[1].click(); + fixture.detectChanges(); + comp.tableRows.get(0).nativeElement.click(); expect(dotRouterService.goToEditContainer).toHaveBeenCalledTimes(1); expect(dotRouterService.goToEditContainer).toHaveBeenCalledWith( containersMock[0].identifier @@ -369,7 +369,13 @@ describe('ContainerListComponent', () => { By.css('.container-listing__header-options p-menu') ).componentInstance; spyOn(dotContainersService, 'publish').and.returnValue(of(mockBulkResponseSuccess)); - comp.updateSelectedContainers(containersMock); + + comp.selectedContainers = containersMock; + + fixture.detectChanges(); + + comp.handleActionMenuOpen({} as MouseEvent); + menu.model[0].command(); expect(dotContainersService.publish).toHaveBeenCalledWith([ '123Published', @@ -402,22 +408,95 @@ describe('ContainerListComponent', () => { expect(dotSiteBrowserService.setSelectedFolder).toHaveBeenCalledWith(path); expect(dotRouterService.goToSiteBrowser).toHaveBeenCalledTimes(1); }); - }); - it('should emit changes in content types selector', () => { - fixture.detectChanges(); - contentTypesSelector = fixture.debugElement.query( - By.css('dot-content-type-selector') - ).componentInstance; - spyOn(comp.listing.paginatorService, 'setExtraParams'); - spyOn(comp.listing, 'loadFirstPage'); - contentTypesSelector.selected.emit('test'); - - expect(comp.listing.paginatorService.setExtraParams).toHaveBeenCalledWith( - 'content_type', - 'test' - ); - expect(comp.listing.loadFirstPage).toHaveBeenCalledWith(); + it('should fetch containers when content types selector changes', () => { + spyOn(store, 'getContainersByContentType'); + fixture.detectChanges(); + + contentTypesSelector = fixture.debugElement.query( + By.css('dot-content-type-selector') + ).componentInstance; + + contentTypesSelector.selected.emit('test'); + + expect(store.getContainersByContentType).toHaveBeenCalledWith('test'); + }); + + it('should fetch containers when archive state change', () => { + spyOn(store, 'getContainersByArchiveState'); + + const headerCheckbox = fixture.debugElement.query( + By.css('[data-testId="archiveCheckbox"]') + ).componentInstance; + + headerCheckbox.onChange.emit({ checked: true }); + + expect(store.getContainersByArchiveState).toHaveBeenCalledWith(true); + }); + + it('should fetch containers when query change', () => { + spyOn(store, 'getContainersByQuery'); + + const queryInput = fixture.debugElement.query( + By.css('[data-testId="query-input"]') + ).nativeElement; + + queryInput.value = 'test'; + queryInput.dispatchEvent(new Event('input')); + + fixture.detectChanges(); + + expect(store.getContainersByQuery).toHaveBeenCalledWith('test'); + }); + + it('should fetch containers with offset when table emits onPage', () => { + spyOn(store, 'getContainersWithOffset'); + + table.onPage.emit({ first: 10 }); + + expect(store.getContainersWithOffset).toHaveBeenCalledWith(10); + }); + + it('should update selectedContainers in store when actions button is clicked', () => { + spyOn(store, 'updateSelectedContainers'); + comp.selectedContainers = [containersMock[0]]; + fixture.detectChanges(); + + const bulkButton = fixture.debugElement.query( + By.css('[data-testId="bulkActions"]') + ).nativeElement; + + bulkButton.click(); + + expect(store.updateSelectedContainers).toHaveBeenCalledWith([containersMock[0]]); + }); + + it('should focus first row when you press arrow down in query input', () => { + spyOn(comp, 'focusFirstRow'); + const queryInput = fixture.debugElement.query( + By.css('[data-testId="query-input"]') + ).nativeElement; + + queryInput.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' })); + + fixture.detectChanges(); + + expect(comp.focusFirstRow).toHaveBeenCalled(); + }); + + it("should fetch containers when site is changed and it's not the first time", () => { + spyOn(paginatorService, 'setExtraParams').and.callThrough(); + + siteService.setFakeCurrentSite(mockSites[1]); + + fixture.detectChanges(); + + expect(paginatorService.setExtraParams).toHaveBeenCalledWith( + 'host', + mockSites[1].identifier + ); + expect(paginatorService.get).toHaveBeenCalled(); + }); }); function setBasicOptions() { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.ts index 57cd96ced1c8..2eaf14ed76c6 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.component.ts @@ -1,16 +1,24 @@ import { Subject } from 'rxjs'; -import { Component, OnDestroy, ViewChild } from '@angular/core'; +import { + Component, + ElementRef, + OnDestroy, + QueryList, + ViewChild, + ViewChildren +} from '@angular/core'; import { DialogService } from 'primeng/dynamicdialog'; +import { Menu } from 'primeng/menu'; -import { takeUntil } from 'rxjs/operators'; +import { skip, takeUntil } from 'rxjs/operators'; import { DotBulkInformationComponent } from '@components/_common/dot-bulk-information/dot-bulk-information.component'; -import { DotListingDataTableComponent } from '@components/dot-listing-data-table/dot-listing-data-table.component'; import { DotMessageSeverity, DotMessageType } from '@components/dot-message-display/model'; import { DotMessageDisplayService } from '@components/dot-message-display/services'; import { DotMessageService } from '@dotcms/data-access'; +import { SiteService } from '@dotcms/dotcms-js'; import { DotActionBulkResult, DotBulkFailItem, @@ -20,7 +28,6 @@ import { } from '@dotcms/dotcms-models'; import { DotActionMenuItem } from '@models/dot-action-menu/dot-action-menu-item.model'; import { DotContainerListStore } from '@portlets/dot-containers/container-list/store/dot-container-list.store'; -import { DotRouterService } from '@services/dot-router/dot-router.service'; @Component({ selector: 'dot-container-list', @@ -29,11 +36,15 @@ import { DotRouterService } from '@services/dot-router/dot-router.service'; providers: [DotContainerListStore] }) export class ContainerListComponent implements OnDestroy { + @ViewChild('actionsMenu') + actionsMenu: Menu; + @ViewChildren('tableRow') + tableRows: QueryList>; + vm$ = this.store.vm$; notify$ = this.store.notify$; - @ViewChild('listing', { static: false }) - listing: DotListingDataTableComponent; + selectedContainers: DotContainer[] = []; private destroy$: Subject = new Subject(); @@ -42,11 +53,16 @@ export class ContainerListComponent implements OnDestroy { private dotMessageService: DotMessageService, private dotMessageDisplayService: DotMessageDisplayService, private dialogService: DialogService, - private dotRouterService: DotRouterService + private siteService: SiteService ) { this.notify$.pipe(takeUntil(this.destroy$)).subscribe(({ payload, message, failsInfo }) => { this.notifyResult(payload, failsInfo, message); + this.selectedContainers = []; }); + + this.siteService.switchSite$ + .pipe(skip(1)) // Skip initialization + .subscribe(({ identifier }) => this.store.getContainersByHost(identifier)); } ngOnDestroy(): void { @@ -60,10 +76,7 @@ export class ContainerListComponent implements OnDestroy { * @memberof ContainerListComponent */ changeContentTypeSelector(value: string) { - value - ? this.listing.paginatorService.setExtraParams('content_type', value) - : this.listing.paginatorService.deleteExtraParams('content_type'); - this.listing.loadFirstPage(); + this.store.getContainersByContentType(value); } /** @@ -92,25 +105,27 @@ export class ContainerListComponent implements OnDestroy { * @memberof ContainerListComponent */ handleArchivedFilter(checked: boolean): void { - checked - ? this.listing.paginatorService.setExtraParams('archive', checked) - : this.listing.paginatorService.deleteExtraParams('archive'); - this.listing.loadFirstPage(); + this.store.getContainersByArchiveState(checked); } /** - * Keep updated the selected containers in the grid - * @param {DotContainer[]} containers + * Handle query filter * + * @param {string} query * @memberof ContainerListComponent */ - updateSelectedContainers(containers: DotContainer[]): void { - const filterContainers = containers.filter( - (container: DotContainer) => - container.identifier !== 'SYSTEM_CONTAINER' && - container.source !== CONTAINER_SOURCE.FILE - ); - this.store.updateSelectedContainers(filterContainers); + handleQueryFilter(query: string): void { + this.store.getContainersByQuery(query); + } + + /** + * Call when click on any pagination link + * @param {LazyLoadEvent} event + * + * @memberof DotContainerListComponent + */ + loadDataPaginationEvent({ first }: { first: number }): void { + this.store.getContainersWithOffset(first); } /** @@ -127,25 +142,41 @@ export class ContainerListComponent implements OnDestroy { } /** - * Return a list of containers with disableInteraction in system items. - * @param {DotContainer[]} containers - * @returns DotContainer[] - * @memberof DotContainerListComponent + * Handle action menu click + * + * @param {MouseEvent} event + * @memberof ContainerListComponent */ - getContainersWithDisabledEntities(containers: DotContainer[]): DotContainer[] { - return containers.map((container) => { - const copyContainer = structuredClone(container); - copyContainer.disableInteraction = - copyContainer.identifier.includes('/') || - copyContainer.identifier === 'SYSTEM_CONTAINER' || - copyContainer.source === CONTAINER_SOURCE.FILE; - - if (copyContainer.path) { - copyContainer.pathName = new URL(`http:${container.path}`).pathname; - } - - return copyContainer; - }); + handleActionMenuOpen(event: MouseEvent): void { + this.updateSelectedContainers(); + this.actionsMenu.toggle(event); + } + + /** + * Focus first row if key arrow down on input + * + * @memberof ContainerListComponent + */ + focusFirstRow(): void { + const { nativeElement: firstActiveRow } = this.tableRows.find( + (row) => row.nativeElement.getAttribute('data-disabled') === 'false' + ) || { nativeElement: null }; // To not break on destructuring + + firstActiveRow?.focus(); + } + + /** + * Keep updated the selected containers in the store + * + * @memberof ContainerListComponent + */ + private updateSelectedContainers(): void { + const filterContainers = this.selectedContainers.filter( + (container: DotContainer) => + container.identifier !== 'SYSTEM_CONTAINER' && + container.source !== CONTAINER_SOURCE.FILE + ); + this.store.updateSelectedContainers(filterContainers); } private notifyResult( @@ -163,8 +194,8 @@ export class ContainerListComponent implements OnDestroy { this.showToastNotification(message); } - this.listing?.clearSelection(); - this.listing?.loadCurrentPage(); + this.store.clearSelectedContainers(); + this.store.loadCurrentContainersPage(); } private showToastNotification(message: string): void { diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.module.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.module.ts index 99c3f66766bb..f09fc27388f9 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.module.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/container-list.module.ts @@ -4,13 +4,15 @@ import { NgModule } from '@angular/core'; import { ButtonModule } from 'primeng/button'; import { CheckboxModule } from 'primeng/checkbox'; import { DialogService } from 'primeng/dynamicdialog'; +import { InputTextModule } from 'primeng/inputtext'; import { MenuModule } from 'primeng/menu'; +import { TableModule } from 'primeng/table'; import { DotActionMenuButtonModule } from '@components/_common/dot-action-menu-button/dot-action-menu-button.module'; import { DotAddToBundleModule } from '@components/_common/dot-add-to-bundle'; import { DotEmptyStateModule } from '@components/_common/dot-empty-state/dot-empty-state.module'; import { DotContentTypeSelectorModule } from '@components/dot-content-type-selector'; -import { DotListingDataTableModule } from '@components/dot-listing-data-table'; +import { ActionHeaderModule } from '@components/dot-listing-data-table/action-header/action-header.module'; import { DotPortletBaseModule } from '@components/dot-portlet-base/dot-portlet-base.module'; import { DotRelativeDatePipe } from '@dotcms/app/view/pipes/dot-relative-date/dot-relative-date.pipe'; import { DotSiteBrowserService } from '@dotcms/data-access'; @@ -27,7 +29,7 @@ import { ContainerListComponent } from './container-list.component'; CommonModule, ContainerListRoutingModule, DotPortletBaseModule, - DotListingDataTableModule, + TableModule, DotContentTypeSelectorModule, DotMessagePipe, ButtonModule, @@ -36,7 +38,9 @@ import { ContainerListComponent } from './container-list.component'; DotEmptyStateModule, DotAddToBundleModule, DotActionMenuButtonModule, - DotRelativeDatePipe + DotRelativeDatePipe, + ActionHeaderModule, + InputTextModule ], providers: [ DotContainerListResolver, diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/store/dot-container-list.store.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/store/dot-container-list.store.ts index 41256eb25c1c..9def5822dec6 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/store/dot-container-list.store.ts +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-containers/container-list/store/dot-container-list.store.ts @@ -1,20 +1,27 @@ import { ComponentStore } from '@ngrx/component-store'; +import { forkJoin } from 'rxjs'; import { Injectable } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { MenuItem } from 'primeng/api'; -import { pluck, take } from 'rxjs/operators'; +import { pluck, switchMap, take, tap } from 'rxjs/operators'; import { DotListingDataTableComponent } from '@components/dot-listing-data-table/dot-listing-data-table.component'; import { DotAlertConfirmService, DotMessageService, - DotSiteBrowserService + DotSiteBrowserService, + PaginatorService } from '@dotcms/data-access'; -import { DotPushPublishDialogService } from '@dotcms/dotcms-js'; -import { DotActionBulkResult, DotBulkFailItem, DotContainer } from '@dotcms/dotcms-models'; +import { DotPushPublishDialogService, SiteService } from '@dotcms/dotcms-js'; +import { + CONTAINER_SOURCE, + DotActionBulkResult, + DotBulkFailItem, + DotContainer +} from '@dotcms/dotcms-models'; import { ActionHeaderOptions } from '@models/action-header'; import { DataTableColumn } from '@models/data-table'; import { DotActionMenuItem } from '@models/dot-action-menu/dot-action-menu-item.model'; @@ -32,6 +39,9 @@ export interface DotContainerListState { stateLabels: { [key: string]: string }; listing: DotListingDataTableComponent; notifyMessages: DotNotifyMessages; + containers: DotContainer[]; + maxPageLinks: number; + totalRecords: number; } export interface DotNotifyMessages { @@ -40,6 +50,8 @@ export interface DotNotifyMessages { failsInfo?: DotBulkFailItem[]; } +const CONTAINERS_URL = 'v1/containers'; + @Injectable() export class DotContainerListStore extends ComponentStore { constructor( @@ -49,33 +61,58 @@ export class DotContainerListStore extends ComponentStore private dotPushPublishDialogService: DotPushPublishDialogService, private dotSiteBrowserService: DotSiteBrowserService, private dotAlertConfirmService: DotAlertConfirmService, - private dotContainersService: DotContainersService + private dotContainersService: DotContainersService, + private paginatorService: PaginatorService, + private dotSiteService: SiteService ) { super(null); - - this.route.data - .pipe(pluck('dotContainerListResolverData'), take(1)) - .subscribe(([isEnterprise, hasEnvironments]: [boolean, boolean]) => { - this.setState({ - containerBulkActions: this.getContainerBulkActions( - hasEnvironments, - isEnterprise - ), - tableColumns: this.getContainerColumns(), - stateLabels: this.getStateLabels(), - isEnterprise: isEnterprise, - hasEnvironments: hasEnvironments, - addToBundleIdentifier: '', - selectedContainers: [], - actionHeaderOptions: this.getActionHeaderOptions(), - listing: {} as DotListingDataTableComponent, - notifyMessages: { - payload: {}, - message: null, - failsInfo: [] - } as DotNotifyMessages - }); - }); + this.paginatorService.url = CONTAINERS_URL; + this.paginatorService.paginationPerPage = 40; + + this.dotSiteService + .getCurrentSite() + .pipe( + take(1), + switchMap(({ identifier }) => { + this.paginatorService.resetExtraParams(); + + this.paginatorService.setExtraParams('host', identifier); + + return forkJoin([ + this.route.data.pipe(pluck('dotContainerListResolverData'), take(1)), + this.paginatorService.get() + ]); + }) + ) + .subscribe( + ([[isEnterprise, hasEnvironments], containers]: [ + [boolean, boolean], + DotContainer[] + ]) => { + this.setState({ + containerBulkActions: this.getContainerBulkActions( + hasEnvironments, + isEnterprise + ), + tableColumns: this.getContainerColumns(), + stateLabels: this.getStateLabels(), + isEnterprise: isEnterprise, + hasEnvironments: hasEnvironments, + addToBundleIdentifier: '', + selectedContainers: [], + actionHeaderOptions: this.getActionHeaderOptions(), + listing: {} as DotListingDataTableComponent, + notifyMessages: { + payload: {}, + message: null, + failsInfo: [] + } as DotNotifyMessages, + containers, + maxPageLinks: this.paginatorService.maxLinksPage, + totalRecords: this.paginatorService.totalRecords + }); + } + ); } readonly vm$ = this.select( @@ -85,7 +122,10 @@ export class DotContainerListStore extends ComponentStore actionHeaderOptions, tableColumns, stateLabels, - selectedContainers + selectedContainers, + containers, + totalRecords, + maxPageLinks }: DotContainerListState) => { return { containerBulkActions, @@ -93,7 +133,10 @@ export class DotContainerListStore extends ComponentStore actionHeaderOptions, tableColumns, stateLabels, - selectedContainers + selectedContainers, + containers, + totalRecords, + maxPageLinks }; } ); @@ -120,6 +163,13 @@ export class DotContainerListStore extends ComponentStore } ); + readonly clearSelectedContainers = this.updater((state: DotContainerListState) => { + return { + ...state, + selectedContainers: [] + }; + }); + readonly updateListing = this.updater( (state: DotContainerListState, listing: DotListingDataTableComponent) => { return { @@ -144,6 +194,86 @@ export class DotContainerListStore extends ComponentStore } ); + readonly getContainersByHost = this.effect((identifier$) => { + return identifier$.pipe( + switchMap((identifier) => { + this.paginatorService.setExtraParams('host', identifier); + + return this.paginatorService.getFirstPage(); + }), + tap((containers: DotContainer[]) => { + this.patchContainers(containers); + }) + ); + }); + + readonly getContainersByContentType = this.effect((contentType$) => { + return contentType$.pipe( + switchMap((contentType) => { + contentType + ? this.paginatorService.setExtraParams('content_type', contentType) + : this.paginatorService.deleteExtraParams('content_type'); + + return this.paginatorService.get(); + }), + tap((containers: DotContainer[]) => { + this.patchContainers(containers); + }) + ); + }); + + readonly getContainersByArchiveState = this.effect((archive$) => { + return archive$.pipe( + switchMap((archive) => { + archive + ? this.paginatorService.setExtraParams('archive', archive) + : this.paginatorService.deleteExtraParams('archive'); + + return this.paginatorService.get(); + }), + tap((containers: DotContainer[]) => { + this.patchContainers(containers); + }) + ); + }); + + readonly getContainersByQuery = this.effect((query$) => { + return query$.pipe( + switchMap((query) => { + query.trim().length + ? this.paginatorService.setExtraParams('filter', query) + : this.paginatorService.deleteExtraParams('filter'); + + return this.paginatorService.get(); + }), + tap((containers: DotContainer[]) => { + this.patchContainers(containers); + }) + ); + }); + + readonly getContainersWithOffset = this.effect((offset$) => { + return offset$.pipe( + switchMap((offset) => { + return this.paginatorService.getWithOffset(offset); + }), + tap((containers: DotContainer[]) => { + this.patchContainers(containers); + }) + ); + }); + + readonly loadCurrentContainersPage = this.effect((origin$) => { + return origin$.pipe( + switchMap(() => { + return this.paginatorService.getCurrentPage(); + }), + tap((containers: DotContainer[]) => { + this.patchContainers(containers); + }) + ); + }); + /** * Set the actions of each container based o current state. * @param {DotContainer} container @@ -546,4 +676,41 @@ export class DotContainerListStore extends ComponentStore return container.identifier === identifier; }).name; } + + /** + * Return a list of containers with disableInteraction in system items. + * @param {DotContainer[]} containers + * @returns DotContainer[] + * @memberof DotContainerListStore + */ + private getContainersWithDisabledEntities(containers: DotContainer[]): DotContainer[] { + return containers.map((container) => { + const copyContainer = structuredClone(container); + copyContainer.disableInteraction = + copyContainer.identifier.includes('/') || + copyContainer.identifier === 'SYSTEM_CONTAINER' || + copyContainer.source === CONTAINER_SOURCE.FILE; + + if (copyContainer.path) { + copyContainer.pathName = new URL(`http:${container.path}`).pathname; + } + + return copyContainer; + }); + } + + /** + * Patch the state with the containers and pagination info. + * + * @private + * @param {DotContainer[]} containers + * @memberof DotContainerListStore + */ + private patchContainers(containers: DotContainer[]): void { + this.patchState({ + containers: this.getContainersWithDisabledEntities(containers), + maxPageLinks: this.paginatorService.maxLinksPage, + totalRecords: this.paginatorService.totalRecords + }); + } } diff --git a/core-web/apps/dotcms-ui/src/app/view/components/dot-content-type-selector/dot-content-type-selector.component.html b/core-web/apps/dotcms-ui/src/app/view/components/dot-content-type-selector/dot-content-type-selector.component.html index e0aee262bd31..bc01b788d68b 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/dot-content-type-selector/dot-content-type-selector.component.html +++ b/core-web/apps/dotcms-ui/src/app/view/components/dot-content-type-selector/dot-content-type-selector.component.html @@ -5,6 +5,7 @@ [showClear]="true" [resetFilterOnHide]="true" (onChange)="change($event)" + appendTo="body" filterBy="label" > diff --git a/core-web/libs/data-access/src/lib/paginator/paginator.service.spec.ts b/core-web/libs/data-access/src/lib/paginator/paginator.service.spec.ts index 7b93c79bb65d..bdd2823d8780 100644 --- a/core-web/libs/data-access/src/lib/paginator/paginator.service.spec.ts +++ b/core-web/libs/data-access/src/lib/paginator/paginator.service.spec.ts @@ -53,6 +53,15 @@ describe('PaginatorService', () => { expect(paginatorService.extraParams.get('name')).toBeUndefined(); }); + it('should remove all extra parameters', () => { + paginatorService.setExtraParams('name', 'John'); + paginatorService.setExtraParams('fullnam', 'John Doe'); + paginatorService.setExtraParams('age', '21'); + paginatorService.resetExtraParams(); + + expect(paginatorService.extraParams.size).toBe(0); + }); + afterEach(() => { httpMock.verify(); }); diff --git a/core-web/libs/data-access/src/lib/paginator/paginator.service.ts b/core-web/libs/data-access/src/lib/paginator/paginator.service.ts index 33bddffbd085..61e546a2e7cb 100644 --- a/core-web/libs/data-access/src/lib/paginator/paginator.service.ts +++ b/core-web/libs/data-access/src/lib/paginator/paginator.service.ts @@ -113,6 +113,15 @@ export class PaginatorService { this.extraParams.delete(name); } + /** + * Reset extra parameters of the eventual request. + * + * @memberof PaginatorService + */ + public resetExtraParams(): void { + this.extraParams.clear(); + } + get extraParams(): Map { return this._extraParams; }