diff --git a/extension.schema.json b/extension.schema.json index d1fac19d2..8664fe4d9 100644 --- a/extension.schema.json +++ b/extension.schema.json @@ -115,6 +115,10 @@ "description": "Element title", "type": "string" }, + "tooltip": { + "description": "Element tooltip to display on hover", + "type": "string" + }, "description": { "description": "Element description, used for the tooltips.", "type": "string" @@ -837,6 +841,12 @@ "items": { "$ref": "#/definitions/contentActionRef" }, "minItems": 1 }, + "bulk-actions": { + "description": "Bulk actions entries", + "type": "array", + "items": { "$ref": "#/definitions/contentActionRef" }, + "minItems": 1 + }, "content-metadata-presets": { "description": "Configuration for the presets for content metadata component", "type": "array", diff --git a/projects/aca-content/assets/i18n/en.json b/projects/aca-content/assets/i18n/en.json index 47896d3b3..32d109265 100644 --- a/projects/aca-content/assets/i18n/en.json +++ b/projects/aca-content/assets/i18n/en.json @@ -501,6 +501,11 @@ } }, "SEARCH": { + "BULK_ACTIONS_DROPDOWN": { + "TITLE": "Bulk Actions ({{ count }} Items)", + "BULK_NOT_AVAILABLE": "Bulk Actions (Not Available)", + "BULK_NOT_AVAILABLE_TOOLTIP": "Bulk Actions cannot be used without search results" + }, "INPUT": { "PLACEHOLDER": "Search", "FILES": "Files", diff --git a/projects/aca-content/src/lib/aca-content.module.ts b/projects/aca-content/src/lib/aca-content.module.ts index 47d6b7482..4182327ba 100644 --- a/projects/aca-content/src/lib/aca-content.module.ts +++ b/projects/aca-content/src/lib/aca-content.module.ts @@ -76,6 +76,7 @@ import { UserMenuComponent } from './components/sidenav/user-menu/user-menu.comp import { ContextMenuComponent } from './components/context-menu/context-menu.component'; import { MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material/dialog'; import { SearchResultsRowComponent } from './components/search/search-results-row/search-results-row.component'; +import { BulkActionsDropdownComponent } from './components/bulk-actions-dropdown/bulk-actions-dropdown.component'; @NgModule({ imports: [ @@ -138,6 +139,7 @@ export class ContentServiceExtensionModule { 'app.toolbar.toggleFavoriteLibrary': ToggleFavoriteLibraryComponent, 'app.toolbar.toggleJoinLibrary': ToggleJoinLibraryButtonComponent, 'app.menu.toggleJoinLibrary': ToggleJoinLibraryMenuComponent, + 'app.bulk-actions-dropdown': BulkActionsDropdownComponent, 'app.shared-link.toggleSharedLink': ToggleSharedComponent, 'app.columns.name': CustomNameColumnComponent, 'app.columns.libraryName': LibraryNameColumnComponent, diff --git a/projects/aca-content/src/lib/components/bulk-actions-dropdown/bulk-actions-dropdown.component.html b/projects/aca-content/src/lib/components/bulk-actions-dropdown/bulk-actions-dropdown.component.html new file mode 100644 index 000000000..e39f6ab1b --- /dev/null +++ b/projects/aca-content/src/lib/components/bulk-actions-dropdown/bulk-actions-dropdown.component.html @@ -0,0 +1,42 @@ + + + + + {{ bulkSelectControl.value?.title | translate }} + + + + + {{ option.title | translate }} + + + diff --git a/projects/aca-content/src/lib/components/bulk-actions-dropdown/bulk-actions-dropdown.component.scss b/projects/aca-content/src/lib/components/bulk-actions-dropdown/bulk-actions-dropdown.component.scss new file mode 100644 index 000000000..8783d39b9 --- /dev/null +++ b/projects/aca-content/src/lib/components/bulk-actions-dropdown/bulk-actions-dropdown.component.scss @@ -0,0 +1,24 @@ +@import '@alfresco/adf-core/lib/styles/mat-selectors'; + +.aca-bulk-actions-form-field { + margin-left: 24px; + margin-top: 1.4375em; + width: 295px; + + #{$mat-form-field-flex}, + #{$mat-form-text-field-infix} { + height: 48px; + line-height: 48px; + display: flex; + align-items: center; + border: 0; + } + + #{$mat-form-field-appearance-outline} #{$mat-form-field-outline} { + top: 0; + } + + #{$mat-select} { + margin-top: 0.25rem; + } +} diff --git a/projects/aca-content/src/lib/components/bulk-actions-dropdown/bulk-actions-dropdown.component.spec.ts b/projects/aca-content/src/lib/components/bulk-actions-dropdown/bulk-actions-dropdown.component.spec.ts new file mode 100644 index 000000000..375d8a842 --- /dev/null +++ b/projects/aca-content/src/lib/components/bulk-actions-dropdown/bulk-actions-dropdown.component.spec.ts @@ -0,0 +1,221 @@ +/*! + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BulkActionsDropdownComponent } from './bulk-actions-dropdown.component'; +import { Store } from '@ngrx/store'; +import { AppStore } from '@alfresco/aca-shared/store'; +import { BehaviorSubject, of } from 'rxjs'; +import { By } from '@angular/platform-browser'; +import { ContentActionRef, ContentActionType } from '@alfresco/adf-extensions'; +import { AppTestingModule } from '../../testing/app-testing.module'; +import { TranslationService } from '@alfresco/adf-core'; +import { AppExtensionService } from '@alfresco/aca-shared'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { MatSelectHarness } from '@angular/material/select/testing'; +import { MatIconRegistry } from '@angular/material/icon'; +import { FakeMatIconRegistry } from '@angular/material/icon/testing'; + +describe('BulkActionsDropdownComponent', () => { + let component: BulkActionsDropdownComponent; + let fixture: ComponentFixture; + let store: Store; + let translationService: TranslationService; + let bulkFormField: HTMLElement; + let dropdown: HTMLElement; + let extensionService: AppExtensionService; + let loader: HarnessLoader; + + const mockItem: ContentActionRef = { + id: 'mockId', + title: 'some title', + tooltip: 'some tooltip', + icon: 'adf:mock-icon', + type: ContentActionType.custom, + actions: { + click: 'TEST_EVENT' + }, + rules: { + visible: 'isItemVisible' + } + }; + + const totalItemsMock$: BehaviorSubject = new BehaviorSubject(0); + + const getElement = (selector: string): HTMLElement | null => fixture.debugElement.query(By.css(`[data-automation-id="${selector}"]`)).nativeElement; + + const getLabelText = (selector: string): string => getElement(selector).textContent.trim(); + + const selectOptionFromDropdown = async (selectionIndex: number) => { + const selectHarness = await loader.getHarness(MatSelectHarness); + await selectHarness.open(); + await selectHarness.clickOptions(await selectHarness.getOptions()[selectionIndex]); + await fixture.whenStable(); + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BulkActionsDropdownComponent, AppTestingModule], + providers: [{ provide: MatIconRegistry, useClass: FakeMatIconRegistry }] + }).compileComponents(); + + store = TestBed.inject(Store); + translationService = TestBed.inject(TranslationService); + + spyOn(store, 'select').and.returnValue(totalItemsMock$); + spyOn(translationService, 'get').and.callFake((key) => of(key)); + + fixture = TestBed.createComponent(BulkActionsDropdownComponent); + component = fixture.componentInstance; + + component.items = [mockItem]; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('when there are no search items', () => { + beforeEach(() => { + totalItemsMock$.next(0); + fixture.detectChanges(); + dropdown = getElement('aca-bulk-actions-dropdown'); + bulkFormField = getElement('aca-bulk-actions-form-field'); + fixture.detectChanges(); + }); + + it('should disable dropdown', () => { + expect(dropdown.getAttribute('aria-disabled')).toBe('true'); + }); + + it('should have correct tooltip', () => { + expect(bulkFormField.getAttribute('title')).toBe('SEARCH.BULK_ACTIONS_DROPDOWN.BULK_NOT_AVAILABLE_TOOLTIP'); + }); + + it('should have correct placeholder', () => { + expect(getLabelText('aca-bulk-actions-dropdown')).toEqual('SEARCH.BULK_ACTIONS_DROPDOWN.BULK_NOT_AVAILABLE'); + }); + + it('should call translationService.get with correct arguments', () => { + expect(translationService.get).toHaveBeenCalledWith('SEARCH.BULK_ACTIONS_DROPDOWN.BULK_NOT_AVAILABLE'); + expect(translationService.get).toHaveBeenCalledWith('SEARCH.BULK_ACTIONS_DROPDOWN.BULK_NOT_AVAILABLE_TOOLTIP'); + }); + }); + + describe('when there are search items', () => { + beforeEach(() => { + totalItemsMock$.next(10); + fixture.detectChanges(); + dropdown = getElement('aca-bulk-actions-dropdown'); + bulkFormField = getElement('aca-bulk-actions-form-field'); + dropdown.click(); + fixture.detectChanges(); + }); + + it('should enable dropdown', () => { + expect(dropdown.getAttribute('aria-disabled')).toBe('false'); + }); + + it('should have correct tooltip', () => { + expect(bulkFormField.getAttribute('title')).toBe('SEARCH.BULK_ACTIONS_DROPDOWN.TITLE'); + }); + + it('should have correct placeholder', () => { + expect(getLabelText('aca-bulk-actions-dropdown')).toEqual('SEARCH.BULK_ACTIONS_DROPDOWN.TITLE'); + }); + + it('should have option with correct tooltip', () => { + const option = getElement('mockId'); + + expect(option.getAttribute('title')).toEqual('some tooltip'); + }); + + it('should have option with correct label', () => { + const optionLabel = getLabelText('mockId'); + + expect(optionLabel).toEqual('some title'); + }); + + it('should have correct icon in an option', () => { + const icon = getElement('aca-bulk-action-icon-mockId'); + + expect(icon.getAttribute('title')).toEqual('some title'); + }); + + it('should call translationService.get with correct arguments', () => { + expect(translationService.get).toHaveBeenCalledWith('SEARCH.BULK_ACTIONS_DROPDOWN.TITLE', { count: 10 }); + }); + + describe('when extension service is used', () => { + beforeEach(() => { + extensionService = TestBed.inject(AppExtensionService); + spyOn(extensionService, 'getBulkActions').and.returnValue(of([mockItem])); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('should run action on selection', async () => { + spyOn(extensionService, 'runActionById'); + await selectOptionFromDropdown(0); + fixture.detectChanges(); + + expect(extensionService.runActionById).toHaveBeenCalledWith(mockItem.actions.click, { + focusedElementOnCloseSelector: '.adf-context-menu-source' + }); + }); + + it('should reset selection on bulkActionExecuted', async () => { + await selectOptionFromDropdown(0); + fixture.detectChanges(); + + expect(component.bulkSelectControl.value).toEqual(mockItem); + + extensionService.bulkActionExecuted(); + fixture.detectChanges(); + + expect(component.bulkSelectControl.value).toBeNull(); + }); + + it('should run dropdown action on Enter', () => { + spyOn(component, 'runAction'); + component.bulkSelectControl.setValue(mockItem); + const selectElement = getElement('aca-bulk-actions-dropdown'); + selectElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + + expect(component.runAction).toHaveBeenCalledWith(mockItem); + }); + + it('should NOT run dropdown action on Tab', () => { + spyOn(component, 'runAction'); + component.bulkSelectControl.setValue(mockItem); + const selectElement = getElement('aca-bulk-actions-dropdown'); + selectElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' })); + + expect(component.runAction).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/projects/aca-content/src/lib/components/bulk-actions-dropdown/bulk-actions-dropdown.component.ts b/projects/aca-content/src/lib/components/bulk-actions-dropdown/bulk-actions-dropdown.component.ts new file mode 100644 index 000000000..9c2983e00 --- /dev/null +++ b/projects/aca-content/src/lib/components/bulk-actions-dropdown/bulk-actions-dropdown.component.ts @@ -0,0 +1,109 @@ +/*! + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { ContentActionRef } from '@alfresco/adf-extensions'; +import { AppStore, getSearchItemsTotalCount } from '@alfresco/aca-shared/store'; +import { CommonModule } from '@angular/common'; +import { Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; +import { MatSelectModule } from '@angular/material/select'; +import { Store } from '@ngrx/store'; +import { TranslateModule } from '@ngx-translate/core'; +import { combineLatest, Observable, Subject } from 'rxjs'; +import { IconComponent, TranslationService } from '@alfresco/adf-core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { switchMap, takeUntil } from 'rxjs/operators'; +import { AppExtensionService } from '@alfresco/aca-shared'; + +@Component({ + standalone: true, + selector: 'aca-bulk-actions-dropdown', + templateUrl: './bulk-actions-dropdown.component.html', + styleUrls: ['./bulk-actions-dropdown.component.scss'], + imports: [CommonModule, TranslateModule, MatSelectModule, IconComponent, ReactiveFormsModule], + encapsulation: ViewEncapsulation.None +}) +export class BulkActionsDropdownComponent implements OnInit, OnDestroy { + @Input() items: ContentActionRef[]; + + placeholder: string; + tooltip: string; + bulkSelectControl = new FormControl(); + + private readonly totalItems$: Observable = this.store.select(getSearchItemsTotalCount); + private readonly onDestroy$ = new Subject(); + + constructor(private store: Store, private translationService: TranslationService, private extensions: AppExtensionService) {} + + ngOnInit() { + this.totalItems$ + .pipe( + switchMap((totalItems) => { + if (totalItems > 0) { + this.bulkSelectControl.enable(); + + return combineLatest([ + this.translationService.get('SEARCH.BULK_ACTIONS_DROPDOWN.TITLE', { count: totalItems }), + this.translationService.get('SEARCH.BULK_ACTIONS_DROPDOWN.TITLE', { count: totalItems }) + ]); + } else { + this.bulkSelectControl.disable(); + + return combineLatest([ + this.translationService.get('SEARCH.BULK_ACTIONS_DROPDOWN.BULK_NOT_AVAILABLE'), + this.translationService.get('SEARCH.BULK_ACTIONS_DROPDOWN.BULK_NOT_AVAILABLE_TOOLTIP') + ]); + } + }), + takeUntil(this.onDestroy$) + ) + .subscribe(([placeholder, title]) => { + this.tooltip = title; + this.placeholder = placeholder; + }); + + this.extensions.bulkActionExecuted$.pipe(takeUntil(this.onDestroy$)).subscribe(() => { + this.bulkSelectControl.setValue(null); + }); + } + + ngOnDestroy() { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } + + runAction(actionOption: ContentActionRef) { + this.extensions.runActionById(actionOption.actions.click, { + focusedElementOnCloseSelector: '.adf-context-menu-source' + }); + } + + onKeyDown(event: KeyboardEvent) { + if (event.key === 'Tab') { + this.bulkSelectControl.setValue(null); + } + if (event.key === 'Enter' && this.bulkSelectControl.value) { + this.runAction(this.bulkSelectControl.value); + } + } +} diff --git a/projects/aca-content/src/lib/components/dl-custom-components/datatable-cell-badges/datatable-cell-badges.component.html b/projects/aca-content/src/lib/components/dl-custom-components/datatable-cell-badges/datatable-cell-badges.component.html new file mode 100644 index 000000000..164f5f55b --- /dev/null +++ b/projects/aca-content/src/lib/components/dl-custom-components/datatable-cell-badges/datatable-cell-badges.component.html @@ -0,0 +1,19 @@ +
+ + + + + + +
diff --git a/projects/aca-content/src/lib/components/dl-custom-components/datatable-cell-badges/datatable-cell-badges.component.scss b/projects/aca-content/src/lib/components/dl-custom-components/datatable-cell-badges/datatable-cell-badges.component.scss new file mode 100644 index 000000000..846801b09 --- /dev/null +++ b/projects/aca-content/src/lib/components/dl-custom-components/datatable-cell-badges/datatable-cell-badges.component.scss @@ -0,0 +1,7 @@ +.aca-datatable-cell-badges-container { + display: flex; + + .adf-datatable-cell-badge { + color: var(--theme-secondary-text); + } +} diff --git a/projects/aca-content/src/lib/components/dl-custom-components/datatable-cell-badges/datatable-cell-badges.component.spec.ts b/projects/aca-content/src/lib/components/dl-custom-components/datatable-cell-badges/datatable-cell-badges.component.spec.ts new file mode 100644 index 000000000..fd957b88d --- /dev/null +++ b/projects/aca-content/src/lib/components/dl-custom-components/datatable-cell-badges/datatable-cell-badges.component.spec.ts @@ -0,0 +1,128 @@ +/*! + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DatatableCellBadgesComponent } from './datatable-cell-badges.component'; +import { AppExtensionService } from '@alfresco/aca-shared'; +import { TranslateModule } from '@ngx-translate/core'; +import { AuthModule } from '@alfresco/adf-core'; +import { Actions } from '@ngrx/effects'; +import { NodeEntry } from '@alfresco/js-api'; +import { of } from 'rxjs'; +import { By } from '@angular/platform-browser'; +import { ContentActionType } from '@alfresco/adf-extensions'; +import { HttpClientModule } from '@angular/common/http'; +import { StoreModule } from '@ngrx/store'; + +const mockNode = { + entry: { + isFile: true, + id: 'nodeId' + } +} as NodeEntry; + +const mockGetBadgesResponse = { + id: 'test', + type: ContentActionType.custom, + icon: 'warning', + tooltip: 'test tooltip' +}; + +describe('DatatableCellBadgesComponent', () => { + let fixture: ComponentFixture; + let component: DatatableCellBadgesComponent; + let appExtensionService: AppExtensionService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + AuthModule.forRoot(), + HttpClientModule, + StoreModule.forRoot( + { app: (state) => state }, + { + initialState: { + app: { + selection: { + nodes: [], + libraries: [], + isEmpty: true, + count: 0 + } + } + } + } + ) + ], + providers: [Actions] + }); + + fixture = TestBed.createComponent(DatatableCellBadgesComponent); + component = fixture.componentInstance; + appExtensionService = TestBed.inject(AppExtensionService); + component.node = mockNode; + }); + + it('should get badges when component initializes', () => { + spyOn(appExtensionService, 'getBadges').and.returnValue(of([mockGetBadgesResponse])); + component.ngOnInit(); + fixture.detectChanges(); + const badges = fixture.debugElement.queryAll(By.css('.adf-datatable-cell-badge')).map((badge) => badge.nativeElement); + expect(appExtensionService.getBadges).toHaveBeenCalled(); + expect(badges.length).toBe(1); + expect(badges[0].innerText).toBe('warning'); + expect(badges[0].attributes['title'].value).toBe('test tooltip'); + }); + + it('should render dynamic component when badge has one provided', () => { + spyOn(appExtensionService, 'getBadges').and.returnValue(of([{ ...mockGetBadgesResponse, component: 'test-id' }])); + component.ngOnInit(); + fixture.detectChanges(); + const dynamicComponent = fixture.debugElement.query(By.css('adf-dynamic-component')).nativeElement; + expect(dynamicComponent).toBeDefined(); + }); + + describe('mouse and keyboard events', () => { + let badges: HTMLElement[]; + let runActionSpy: jasmine.Spy; + beforeEach(() => { + spyOn(appExtensionService, 'getBadges').and.returnValue(of([{ ...mockGetBadgesResponse, actions: { click: 'test' } }])); + component.ngOnInit(); + fixture.detectChanges(); + badges = fixture.debugElement.queryAll(By.css('.adf-datatable-cell-badge')).map((badge) => badge.nativeElement); + runActionSpy = spyOn(appExtensionService, 'runActionById'); + }); + + it('should call provided handler on click', () => { + badges[0].click(); + expect(runActionSpy).toHaveBeenCalledWith('test', component.node); + }); + + it('should call provided handler on keypress event', () => { + badges[0].dispatchEvent(new KeyboardEvent('keypress.enter')); + expect(runActionSpy).toHaveBeenCalledWith('test', component.node); + }); + }); +}); diff --git a/projects/aca-content/src/lib/components/dl-custom-components/datatable-cell-badges/datatable-cell-badges.component.ts b/projects/aca-content/src/lib/components/dl-custom-components/datatable-cell-badges/datatable-cell-badges.component.ts new file mode 100644 index 000000000..024b25ae9 --- /dev/null +++ b/projects/aca-content/src/lib/components/dl-custom-components/datatable-cell-badges/datatable-cell-badges.component.ts @@ -0,0 +1,76 @@ +/*! + * Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Alfresco Example Content Application + * + * This file is part of the Alfresco Example Content Application. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * The Alfresco Example Content Application is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Alfresco Example Content Application is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * from Hyland Software. If not, see . + */ + +import { AppExtensionService, Badge } from '@alfresco/aca-shared'; +import { IconComponent } from '@alfresco/adf-core'; +import { DynamicExtensionComponent } from '@alfresco/adf-extensions'; +import { NodeEntry } from '@alfresco/js-api'; +import { CommonModule } from '@angular/common'; +import { Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'aca-datatable-cell-badges', + templateUrl: './datatable-cell-badges.component.html', + styleUrls: ['./datatable-cell-badges.component.scss'], + host: { class: 'aca-datatable-cell-badges' }, + encapsulation: ViewEncapsulation.None, + imports: [CommonModule, TranslateModule, DynamicExtensionComponent, IconComponent], + standalone: true +}) +export class DatatableCellBadgesComponent implements OnInit, OnDestroy { + @Input() node: NodeEntry; + + badges: Badge[]; + + private onDestroy$ = new Subject(); + + constructor(private appExtensionService: AppExtensionService) {} + + ngOnInit() { + this.appExtensionService + .getBadges(this.node) + .pipe(takeUntil(this.onDestroy$)) + .subscribe((badges) => { + this.badges = badges; + }); + } + + ngOnDestroy() { + this.onDestroy$.next(true); + this.onDestroy$.complete(); + } + + onBadgeClick(badge: Badge) { + if (badge.actions?.click) { + this.appExtensionService.runActionById(badge.actions?.click, this.node); + } + } + + onKeyPress(badge: Badge) { + this.onBadgeClick(badge); + } +} diff --git a/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.html b/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.html index 6edbe0723..27cea1da6 100644 --- a/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.html +++ b/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.html @@ -19,12 +19,5 @@ -
- - - - - - -
+ diff --git a/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.scss b/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.scss index 395de95fa..00862e023 100644 --- a/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.scss +++ b/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.scss @@ -4,14 +4,6 @@ justify-content: space-between; width: 100%; - .aca-name-column-badges { - display: flex; - - .adf-datatable-cell-badge { - color: var(--theme-secondary-text); - } - } - .aca-name-column-container { white-space: nowrap; overflow: hidden; @@ -36,6 +28,6 @@ position: unset; } -.adf-datatable-list .adf-datatable-link:hover .aca-name-column-badges { +.adf-datatable-list .adf-datatable-link:hover .aca-datatable-cell-badges { color: var(--adf-theme-foreground-text-color); } diff --git a/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.spec.ts b/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.spec.ts index 54ccbb3db..d307248ed 100644 --- a/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.spec.ts +++ b/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.spec.ts @@ -28,16 +28,23 @@ import { StoreModule } from '@ngrx/store'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { HttpClientModule } from '@angular/common/http'; import { TranslateModule } from '@ngx-translate/core'; -import { AppExtensionService } from '@alfresco/aca-shared'; -import { of } from 'rxjs'; -import { ContentActionType } from '@alfresco/adf-extensions'; import { By } from '@angular/platform-browser'; import { AuthModule } from '@alfresco/adf-core'; +import { Component, Input } from '@angular/core'; +import { NodeEntry } from '@alfresco/js-api'; + +@Component({ + selector: 'aca-datatable-cell-badges', + standalone: true, + template: '' +}) +class MockDatatableCellBadgesComponent { + @Input() node: NodeEntry; +} describe('CustomNameColumnComponent', () => { let fixture: ComponentFixture; let component: CustomNameColumnComponent; - let appExtensionService: AppExtensionService; beforeEach(() => { TestBed.configureTestingModule({ @@ -45,6 +52,7 @@ describe('CustomNameColumnComponent', () => { HttpClientModule, TranslateModule.forRoot(), CustomNameColumnComponent, + MockDatatableCellBadgesComponent, AuthModule.forRoot(), StoreModule.forRoot( { app: (state) => state }, @@ -67,7 +75,6 @@ describe('CustomNameColumnComponent', () => { fixture = TestBed.createComponent(CustomNameColumnComponent); component = fixture.componentInstance; - appExtensionService = TestBed.inject(AppExtensionService); }); it('should not render lock element if file is not locked', () => { @@ -142,54 +149,9 @@ describe('CustomNameColumnComponent', () => { expect(event.stopPropagation).toHaveBeenCalled(); }); - describe('Name column badges', () => { - beforeEach(() => { - component.context = { - row: { - node: { - entry: { - isFile: true, - id: 'nodeId' - } - }, - getValue: (key: string) => key - } - }; - }); - - it('should get badges when component initializes', () => { - spyOn(appExtensionService, 'getBadges').and.returnValue( - of([{ id: 'test', type: ContentActionType.custom, icon: 'warning', tooltip: 'test tooltip' }]) - ); - component.ngOnInit(); - fixture.detectChanges(); - const badges = fixture.debugElement.queryAll(By.css('.adf-datatable-cell-badge')).map((badge) => badge.nativeElement); - expect(appExtensionService.getBadges).toHaveBeenCalled(); - expect(badges.length).toBe(1); - expect(badges[0].innerText).toBe('warning'); - expect(badges[0].attributes['title'].value).toBe('test tooltip'); - }); - - it('should call provided handler on click', () => { - spyOn(appExtensionService, 'runActionById'); - spyOn(appExtensionService, 'getBadges').and.returnValue( - of([{ id: 'test', type: ContentActionType.custom, icon: 'warning', tooltip: 'test tooltip', actions: { click: 'test' } }]) - ); - component.ngOnInit(); - fixture.detectChanges(); - const badges = fixture.debugElement.queryAll(By.css('.adf-datatable-cell-badge')).map((badge) => badge.nativeElement); - badges[0].click(); - expect(appExtensionService.runActionById).toHaveBeenCalledWith('test', component.context.row.node); - }); - - it('should render dynamic component when badge has one provided', () => { - spyOn(appExtensionService, 'getBadges').and.returnValue( - of([{ id: 'test', type: ContentActionType.custom, icon: 'warning', tooltip: 'test tooltip', component: 'test-id' }]) - ); - component.ngOnInit(); - fixture.detectChanges(); - const dynamicComponent = fixture.debugElement.query(By.css('adf-dynamic-component')).nativeElement; - expect(dynamicComponent).toBeDefined(); - }); + it('should pass node to badge component', () => { + const badgeElement = fixture.debugElement.query(By.css('aca-datatable-cell-badges')); + expect(badgeElement).not.toBe(null); + expect(badgeElement.componentInstance.node).toBe(component.node); }); }); diff --git a/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.ts b/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.ts index c85da82c4..7938dd124 100644 --- a/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.ts +++ b/projects/aca-content/src/lib/components/dl-custom-components/name-column/name-column.component.ts @@ -28,15 +28,24 @@ import { Actions, ofType } from '@ngrx/effects'; import { Subject } from 'rxjs'; import { filter, takeUntil } from 'rxjs/operators'; import { NodeActionTypes } from '@alfresco/aca-shared/store'; -import { LockedByComponent, isLocked, AppExtensionService, Badge } from '@alfresco/aca-shared'; +import { LockedByComponent, isLocked } from '@alfresco/aca-shared'; import { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; import { IconComponent } from '@alfresco/adf-core'; import { DynamicExtensionComponent } from '@alfresco/adf-extensions'; +import { DatatableCellBadgesComponent } from '../datatable-cell-badges/datatable-cell-badges.component'; @Component({ standalone: true, - imports: [CommonModule, TranslateModule, LockedByComponent, IconComponent, NodeNameTooltipPipe, DynamicExtensionComponent], + imports: [ + CommonModule, + TranslateModule, + LockedByComponent, + IconComponent, + NodeNameTooltipPipe, + DynamicExtensionComponent, + DatatableCellBadgesComponent + ], selector: 'aca-custom-name-column', templateUrl: './name-column.component.html', styleUrls: ['./name-column.component.scss'], @@ -50,15 +59,8 @@ export class CustomNameColumnComponent extends NameColumnComponent implements On isFile: boolean; isFileWriteLocked: boolean; - badges: Badge[]; - constructor( - element: ElementRef, - private cd: ChangeDetectorRef, - private actions$: Actions, - private nodesService: NodesApiService, - private appExtensionService: AppExtensionService - ) { + constructor(element: ElementRef, private cd: ChangeDetectorRef, private actions$: Actions, private nodesService: NodesApiService) { super(element, nodesService); } @@ -95,13 +97,6 @@ export class CustomNameColumnComponent extends NameColumnComponent implements On this.isFileWriteLocked = isLocked(this.node); this.cd.detectChanges(); }); - - this.appExtensionService - .getBadges(this.node) - .pipe(takeUntil(this.onDestroy$$)) - .subscribe((badges) => { - this.badges = badges; - }); } onLinkClick(event: Event) { @@ -115,10 +110,4 @@ export class CustomNameColumnComponent extends NameColumnComponent implements On this.onDestroy$$.next(true); this.onDestroy$$.complete(); } - - onBadgeClick(badge: Badge) { - if (badge.actions?.click) { - this.appExtensionService.runActionById(badge.actions?.click, this.node); - } - } } diff --git a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.html b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.html index 7358cd2ad..213e80be8 100644 --- a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.html +++ b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.html @@ -1,38 +1,41 @@ -
- - - +
+ + + +
+
+ [title]="descriptionStripped" + [innerHTML]="description$ | async" + >
+
+ +
+
-
-
- -
-
+ diff --git a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.scss b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.scss index 603480cb7..11035049a 100644 --- a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.scss +++ b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.scss @@ -1,4 +1,7 @@ .aca-search-results-row { + display: flex; + justify-content: space-between; + align-items: center; padding: 10px 0; width: inherit; diff --git a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.spec.ts b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.spec.ts index 9c64b704e..2d028125e 100644 --- a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.spec.ts +++ b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.spec.ts @@ -28,6 +28,16 @@ import { By } from '@angular/platform-browser'; import { first } from 'rxjs/operators'; import { AppTestingModule } from '../../../testing/app-testing.module'; import { SearchResultsRowComponent } from './search-results-row.component'; +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'aca-datatable-cell-badges', + standalone: true, + template: '' +}) +class MockDatatableCellBadgesComponent { + @Input() node: NodeEntry; +} describe('SearchResultsRowComponent', () => { let component: SearchResultsRowComponent; @@ -76,7 +86,7 @@ describe('SearchResultsRowComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [AppTestingModule, SearchResultsRowComponent] + imports: [AppTestingModule, SearchResultsRowComponent, MockDatatableCellBadgesComponent] }); fixture = TestBed.createComponent(SearchResultsRowComponent); @@ -120,4 +130,11 @@ describe('SearchResultsRowComponent', () => { }); fixture.detectChanges(); }); + + it('should pass node to badge component', () => { + component.context = { row: { node: nodeEntry } }; + const badgeElement = fixture.debugElement.query(By.css('aca-datatable-cell-badges')); + expect(badgeElement).not.toBe(null); + expect(badgeElement.componentInstance.node).toBe(component.context.node); + }); }); diff --git a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.ts b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.ts index 7dffa6b21..c7410a3da 100644 --- a/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.ts +++ b/projects/aca-content/src/lib/components/search/search-results-row/search-results-row.component.ts @@ -24,7 +24,7 @@ import { Component, Input, OnInit, ViewEncapsulation, ChangeDetectionStrategy, OnDestroy, inject } from '@angular/core'; import { NodeEntry, SearchEntryHighlight } from '@alfresco/js-api'; -import { ViewNodeAction, NavigateToFolder } from '@alfresco/aca-shared/store'; +import { NavigateToFolder, ViewNodeAction } from '@alfresco/aca-shared/store'; import { Store } from '@ngrx/store'; import { BehaviorSubject, Subject } from 'rxjs'; import { NodesApiService } from '@alfresco/adf-content-services'; @@ -34,10 +34,11 @@ import { AutoDownloadService, AppSettingsService } from '@alfresco/aca-shared'; import { CommonModule } from '@angular/common'; import { LocationLinkComponent } from '../../common/location-link/location-link.component'; import { MatDialogModule } from '@angular/material/dialog'; +import { DatatableCellBadgesComponent } from '../../dl-custom-components/datatable-cell-badges/datatable-cell-badges.component'; @Component({ standalone: true, - imports: [CommonModule, LocationLinkComponent, MatDialogModule], + imports: [CommonModule, LocationLinkComponent, MatDialogModule, DatatableCellBadgesComponent], selector: 'aca-search-results-row', templateUrl: './search-results-row.component.html', styleUrls: ['./search-results-row.component.scss'], @@ -65,7 +66,6 @@ export class SearchResultsRowComponent implements OnInit, OnDestroy { titleStripped = ''; descriptionStripped = ''; contentStripped = ''; - isFile = false; constructor( diff --git a/projects/aca-content/src/lib/components/search/search-results/search-results.component.html b/projects/aca-content/src/lib/components/search/search-results/search-results.component.html index aa05dbe1f..a999f2082 100644 --- a/projects/aca-content/src/lib/components/search/search-results/search-results.component.html +++ b/projects/aca-content/src/lib/components/search/search-results/search-results.component.html @@ -1,6 +1,7 @@
+
diff --git a/projects/aca-content/src/lib/components/search/search-results/search-results.component.ts b/projects/aca-content/src/lib/components/search/search-results/search-results.component.ts index bddad0fde..2ddf74b44 100644 --- a/projects/aca-content/src/lib/components/search/search-results/search-results.component.ts +++ b/projects/aca-content/src/lib/components/search/search-results/search-results.component.ts @@ -39,7 +39,8 @@ import { NavigateToFolder, SetInfoDrawerPreviewStateAction, SetInfoDrawerStateAction, - ShowInfoDrawerPreviewAction + ShowInfoDrawerPreviewAction, + SetSearchItemsTotalCountAction } from '@alfresco/aca-shared/store'; import { CustomEmptyContentTemplateDirective, @@ -75,6 +76,7 @@ import { TagsColumnComponent } from '../../dl-custom-components/tags-column/tags import { MatIconModule } from '@angular/material/icon'; import { SearchResultsRowComponent } from '../search-results-row/search-results-row.component'; import { DocumentListPresetRef, DynamicColumnComponent } from '@alfresco/adf-extensions'; +import { BulkActionsDropdownComponent } from '../../bulk-actions-dropdown/bulk-actions-dropdown.component'; @Component({ standalone: true, @@ -107,7 +109,8 @@ import { DocumentListPresetRef, DynamicColumnComponent } from '@alfresco/adf-ext DataColumnComponent, DateColumnHeaderComponent, CustomEmptyContentTemplateDirective, - ViewerToolbarComponent + ViewerToolbarComponent, + BulkActionsDropdownComponent ], selector: 'aca-search-results', templateUrl: './search-results.component.html', @@ -268,6 +271,7 @@ export class SearchResultsComponent extends PageComponent implements OnInit { onSearchResultLoaded(nodePaging: ResultSetPaging) { this.data = nodePaging; this.totalResults = this.getNumberOfResults(); + this.store.dispatch(new SetSearchItemsTotalCountAction(this.totalResults)); } getNumberOfResults() { diff --git a/projects/aca-content/src/lib/store/reducers/app.reducer.ts b/projects/aca-content/src/lib/store/reducers/app.reducer.ts index 192d8398d..aa62580c8 100644 --- a/projects/aca-content/src/lib/store/reducers/app.reducer.ts +++ b/projects/aca-content/src/lib/store/reducers/app.reducer.ts @@ -38,7 +38,8 @@ import { SetInfoDrawerPreviewStateAction, AppActionTypes, ShowLoaderAction, - INITIAL_APP_STATE + INITIAL_APP_STATE, + SetSearchItemsTotalCountAction } from '@alfresco/aca-shared/store'; export function appReducer(state: AppState = INITIAL_APP_STATE, action: Action): AppState { @@ -84,6 +85,12 @@ export function appReducer(state: AppState = INITIAL_APP_STATE, action: Action): case AppActionTypes.ShowLoaderAction: newState = showLoader(state, action as ShowLoaderAction); break; + case AppActionTypes.SetSearchItemsTotalCount: + newState = { + ...state, + searchItemsTotalCount: (action as SetSearchItemsTotalCountAction).payload + }; + break; default: newState = { ...state }; } diff --git a/projects/aca-shared/src/lib/components/document-base-page/document-base-page.component.ts b/projects/aca-shared/src/lib/components/document-base-page/document-base-page.component.ts index 18648342c..f404b68f3 100644 --- a/projects/aca-shared/src/lib/components/document-base-page/document-base-page.component.ts +++ b/projects/aca-shared/src/lib/components/document-base-page/document-base-page.component.ts @@ -61,12 +61,13 @@ export abstract class PageComponent implements OnInit, OnDestroy, OnChanges { selection: SelectionState; actions: Array = []; viewerToolbarActions: Array = []; + bulkActions: ContentActionRef[] = []; canUpdateNode = false; canUpload = false; nodeResult: NodePaging; showHeader = ShowHeaderMode.Data; filterSorting = 'name-asc'; - createActions: Array = []; + createActions: ContentActionRef[] = []; isSmallScreen = false; selectedRowItemsCount = 0; @@ -107,6 +108,13 @@ export abstract class PageComponent implements OnInit, OnDestroy, OnChanges { this.actions = actions; }); + this.extensions + .getBulkActions() + .pipe(takeUntil(this.onDestroy$)) + .subscribe((actions) => { + this.bulkActions = actions; + }); + this.extensions .getViewerToolbarActions() .pipe(takeUntil(this.onDestroy$)) diff --git a/projects/aca-shared/src/lib/models/types.ts b/projects/aca-shared/src/lib/models/types.ts index 9ea7af378..fe39c7223 100644 --- a/projects/aca-shared/src/lib/models/types.ts +++ b/projects/aca-shared/src/lib/models/types.ts @@ -29,6 +29,8 @@ export interface ExtensionRoute extends Route { parentRoute?: string; } -export interface Badge extends ContentActionRef { +export interface Badge extends Partial> { + id: string; + icon: string; tooltip: string; } diff --git a/projects/aca-shared/src/lib/services/app.extension.service.spec.ts b/projects/aca-shared/src/lib/services/app.extension.service.spec.ts index 75ce669ed..1b9af0366 100644 --- a/projects/aca-shared/src/lib/services/app.extension.service.spec.ts +++ b/projects/aca-shared/src/lib/services/app.extension.service.spec.ts @@ -88,6 +88,16 @@ describe('AppExtensionService', () => { } }; + const defaultConfigMock = { + $id: 'test', + $name: 'test', + $version: '1.0.0', + $license: 'MIT', + $vendor: 'Good company', + $runtime: '1.5.0', + features: {} + } as ExtensionConfig; + describe('configs', () => { it('should log an error during setup', async () => { spyOn(extensions, 'load').and.returnValue(Promise.resolve(null)); @@ -100,12 +110,7 @@ describe('AppExtensionService', () => { it('should load content metadata presets', () => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { 'content-metadata-presets': [ { @@ -180,12 +185,7 @@ describe('AppExtensionService', () => { it('should support column orders', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { documentList: { files: [ @@ -250,12 +250,7 @@ describe('AppExtensionService', () => { it('should ignore column if visibility in rules is false', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { documentList: { files: [ @@ -301,12 +296,7 @@ describe('AppExtensionService', () => { describe('actions', () => { beforeEach(() => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, actions: [ { id: 'aca:actions/create-folder', @@ -415,12 +405,7 @@ describe('AppExtensionService', () => { describe('content actions', () => { it('should load content actions from the config', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { toolbar: [ { @@ -447,12 +432,7 @@ describe('AppExtensionService', () => { it('should sort content actions by order', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { toolbar: [ { @@ -483,12 +463,7 @@ describe('AppExtensionService', () => { describe('open with', () => { it('should load [open with] actions for the viewer', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { viewer: { openWith: [ @@ -516,12 +491,7 @@ describe('AppExtensionService', () => { it('should load only enabled [open with] actions for the viewer', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { viewer: { openWith: [ @@ -560,12 +530,7 @@ describe('AppExtensionService', () => { it('should sort [open with] actions by order', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { viewer: { openWith: [ @@ -606,12 +571,7 @@ describe('AppExtensionService', () => { describe('create', () => { it('should load [create] actions from config', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { create: [ { @@ -633,12 +593,7 @@ describe('AppExtensionService', () => { it('should sort [create] actions by order', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { create: [ { @@ -876,12 +831,7 @@ describe('AppExtensionService', () => { applyConfig( { - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { viewer: { shared: { @@ -905,14 +855,7 @@ describe('AppExtensionService', () => { appConfigService.config = { auth: { withCredentials: true } }; - applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0' - }); + applyConfig(defaultConfigMock); expect(service.withCredentials).toBe(true); }); @@ -921,28 +864,14 @@ describe('AppExtensionService', () => { appConfigService.config = { auth: { withCredentials: false } }; - applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0' - }); + applyConfig(defaultConfigMock); expect(service.withCredentials).toBe(false); }); it('should set `withCredentials` to false as default value if no app configuration', () => { appConfigService.config = {}; - applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0' - }); + applyConfig(defaultConfigMock); expect(service.withCredentials).toBe(false); }); @@ -951,12 +880,7 @@ describe('AppExtensionService', () => { describe('getHeaderActions', () => { it('should load user actions from the config', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { header: [ { @@ -981,12 +905,7 @@ describe('AppExtensionService', () => { it('should sort header actions by order', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { header: [ { @@ -1013,12 +932,7 @@ describe('AppExtensionService', () => { it('should sort header menu children actions by order', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { header: [ { @@ -1060,12 +974,7 @@ describe('AppExtensionService', () => { notVisible: () => false }); config = { - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { search: [ { @@ -1361,12 +1270,7 @@ describe('AppExtensionService', () => { it('should set the action disabled for create actions', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { create: actions } @@ -1380,12 +1284,7 @@ describe('AppExtensionService', () => { it('should set the action disabled for sidebar actions', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { sidebar: { toolbar: actions @@ -1401,12 +1300,7 @@ describe('AppExtensionService', () => { it('should set the action disabled for toolbar actions', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { toolbar: actions } @@ -1420,12 +1314,7 @@ describe('AppExtensionService', () => { it('should set the action disabled for viewer toolbar actions', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { viewer: { toolbarActions: actions } } @@ -1440,12 +1329,7 @@ describe('AppExtensionService', () => { it('should set the action disabled for shared link viewer toolbar actions', (done) => { applyConfig( { - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { viewer: { shared: { @@ -1465,12 +1349,7 @@ describe('AppExtensionService', () => { it('should set the action disabled for header actions', (done) => { applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { header: actions } @@ -1482,15 +1361,27 @@ describe('AppExtensionService', () => { }); }); + it('should set the action disabled for bulk actions dropdown actions', (done) => { + applyConfig( + { + ...defaultConfigMock, + features: { + 'bulk-actions': actions + } + }, + true + ); + + service.getBulkActions().subscribe((bulkActions) => { + expect(bulkActions).toEqual(expectedActionsWithoutChildren); + done(); + }); + }); + it('should set the action disabled for context menu actions', (done) => { applyConfig( { - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { contextMenu: actions } @@ -1524,12 +1415,7 @@ describe('AppExtensionService', () => { ]; config = { - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { contextMenu: [...actions], toolbar: [...actions], @@ -1629,12 +1515,7 @@ describe('AppExtensionService', () => { const rawUrl = './assets/images/ft_ic_ms_excel.svg'; applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { icons: [ { @@ -1653,12 +1534,7 @@ describe('AppExtensionService', () => { const warn = spyOn(logService, 'warn').and.stub(); applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { icons: [ { @@ -1675,12 +1551,7 @@ describe('AppExtensionService', () => { const warn = spyOn(logService, 'warn').and.stub(); applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { icons: [ { @@ -1701,12 +1572,7 @@ describe('AppExtensionService', () => { }); applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { badges: [ { @@ -1759,12 +1625,7 @@ describe('AppExtensionService', () => { }); applyConfig({ - $id: 'test', - $name: 'test', - $version: '1.0.0', - $license: 'MIT', - $vendor: 'Good company', - $runtime: '1.5.0', + ...defaultConfigMock, features: { customMetadataPanels: [ { @@ -1814,4 +1675,14 @@ describe('AppExtensionService', () => { service.updateSidebarActions(); expect(loader.getContentActions).toHaveBeenCalledWith(service.config, 'features.sidebar.toolbar'); }); + + it('should emit bulkActionExecuted', (done) => { + spyOn(service, 'bulkActionExecuted').and.callThrough(); + service.bulkActionExecuted$.subscribe(() => { + expect(service.bulkActionExecuted).toHaveBeenCalled(); + done(); + }); + + service.bulkActionExecuted(); + }); }); diff --git a/projects/aca-shared/src/lib/services/app.extension.service.ts b/projects/aca-shared/src/lib/services/app.extension.service.ts index 24e2fcf8e..53460b5c7 100644 --- a/projects/aca-shared/src/lib/services/app.extension.service.ts +++ b/projects/aca-shared/src/lib/services/app.extension.service.ts @@ -50,7 +50,7 @@ import { sortByOrder } from '@alfresco/adf-extensions'; import { AppConfigService, AuthenticationService, LogService } from '@alfresco/adf-core'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { NodeEntry, RepositoryInfo } from '@alfresco/js-api'; import { ViewerRules } from '../models/viewer.rules'; import { Badge } from '../models/types'; @@ -63,6 +63,7 @@ import { SearchCategory } from '@alfresco/adf-content-services'; }) export class AppExtensionService implements RuleContext { private _references = new BehaviorSubject([]); + bulkActionExecuted$ = new Subject(); navbar: Array = []; sidebarTabs: Array = []; @@ -81,6 +82,7 @@ export class AppExtensionService implements RuleContext { private _badges = new BehaviorSubject>([]); private _filesDocumentListPreset = new BehaviorSubject>([]); private _customMetadataPanels = new BehaviorSubject>([]); + private _bulkActions = new BehaviorSubject>([]); documentListPresets: { libraries: Array; @@ -160,6 +162,7 @@ export class AppExtensionService implements RuleContext { this._badges.next(this.loader.getElements(config, 'features.badges')); this._filesDocumentListPreset.next(this.getDocumentListPreset(config, 'files')); this._customMetadataPanels.next(this.loader.getElements(config, 'features.customMetadataPanels')); + this._bulkActions.next(this.loader.getElements(config, 'features.bulk-actions')); this.navbar = this.loadNavBar(config); this.sidebarTabs = this.loader.getElements(config, 'features.sidebar.tabs'); @@ -424,6 +427,10 @@ export class AppExtensionService implements RuleContext { return this._viewerToolbarActions.pipe(map((viewerToolbarActions) => this.getAllowedActions(viewerToolbarActions))); } + getBulkActions(): Observable> { + return this._bulkActions.pipe(map((bulkActions) => this.getAllowedActions(bulkActions))); + } + getOpenWithActions(): Observable> { return this._openWithActions.pipe(map((openWithActions) => this.getAllowedActions(openWithActions))); } @@ -565,4 +572,8 @@ export class AppExtensionService implements RuleContext { return true; } + + bulkActionExecuted(): void { + this.bulkActionExecuted$.next(); + } } diff --git a/projects/aca-shared/store/src/actions/app-action-types.ts b/projects/aca-shared/store/src/actions/app-action-types.ts index 7a892e176..0540d3ebb 100644 --- a/projects/aca-shared/store/src/actions/app-action-types.ts +++ b/projects/aca-shared/store/src/actions/app-action-types.ts @@ -36,5 +36,6 @@ export enum AppActionTypes { SetFileUploadingDialog = 'SET_FILE_UPLOADING_DIALOG', ShowInfoDrawerPreview = 'SHOW_INFO_DRAWER_PREVIEW', SetInfoDrawerPreviewState = 'SET_INFO_DRAWER_PREVIEW_STATE', - ShowLoaderAction = 'SHOW_LOADER' + ShowLoaderAction = 'SHOW_LOADER', + SetSearchItemsTotalCount = 'SET_SEARCH_ITEMS_TOTAL_COUNT' } diff --git a/projects/aca-shared/store/src/actions/app.actions.ts b/projects/aca-shared/store/src/actions/app.actions.ts index 25141eb0f..d1c0aba62 100644 --- a/projects/aca-shared/store/src/actions/app.actions.ts +++ b/projects/aca-shared/store/src/actions/app.actions.ts @@ -101,3 +101,9 @@ export class ShowLoaderAction implements Action { constructor(public payload: boolean) {} } + +export class SetSearchItemsTotalCountAction implements Action { + readonly type = AppActionTypes.SetSearchItemsTotalCount; + + constructor(public payload: number) {} +} diff --git a/projects/aca-shared/store/src/selectors/app.selectors.ts b/projects/aca-shared/store/src/selectors/app.selectors.ts index 54639b422..e7babba33 100644 --- a/projects/aca-shared/store/src/selectors/app.selectors.ts +++ b/projects/aca-shared/store/src/selectors/app.selectors.ts @@ -42,6 +42,7 @@ export const isHXIConnectorEnabled = createSelector(getRepositoryStatus, (info) export const isAdmin = createSelector(selectApp, (state) => state.user.isAdmin); export const getFileUploadingDialog = createSelector(selectApp, (state) => state.fileUploadingDialog); export const showLoaderSelector = createSelector(selectApp, (state) => state.showLoader); +export const getSearchItemsTotalCount = createSelector(selectApp, (state) => state.searchItemsTotalCount); export const getSideNavState = createSelector(getAppSelection, getNavigationState, (selection, navigation) => ({ selection, diff --git a/projects/aca-shared/store/src/states/app.state.ts b/projects/aca-shared/store/src/states/app.state.ts index e19ca048f..af9f2ce30 100644 --- a/projects/aca-shared/store/src/states/app.state.ts +++ b/projects/aca-shared/store/src/states/app.state.ts @@ -55,7 +55,8 @@ export const INITIAL_APP_STATE: AppState = { status: { isQuickShareEnabled: true } - } as any + } as any, + searchItemsTotalCount: null }; /** @deprecated no longer used */ @@ -74,6 +75,7 @@ export interface AppState { repository: RepositoryInfo; fileUploadingDialog: boolean; showLoader: boolean; + searchItemsTotalCount: number; } export interface AppStore {