[ACS-8036] [Bulk update] [Many files to 1 Hold flow] "Manage Holds" dialog with Manage Existing Holds/Apply New Hold tabs (#4019)

* [ACS-8325] [Bulk Legal Hold] Create Bulk Actions Dropdown (#3956)

* ACS-8036 create bulk-actions-dropdown

* ACS-8325 update names and add unit tests

* ACS-8325 added icon and unit tests

* ACS-8325 fix translation

* ACS-8325 add and refactor tests in app extension service

* ACS-8325 resolve conversations: remove loadBulkActions, rename bulk schema, add translations

* ACS-8325 update state

* ACS-8325 temporary remove disabled state as it doesn't work after Angular migration

* ACS-8325 add formControl and tests

* ACS-8325 clean code

* ACS-8325 place create component to one place

* ACS-8325 add condition to use title if no description provided and cover with test

* ACS-8325 add tooltip and update Badge interface

* ACS-8325 rename class names and mockItem object

* [ACS-8326] enable running actions from bulk dropdown and resetting selection (#3971)

* ACS-8036 create bulk-actions-dropdown

* ACS-8325 update names and add unit tests

* ACS-8325 fix translation

* ACS-8325 resolve conversations: remove loadBulkActions, rename bulk schema, add translations

* ACS-8325 temporary remove disabled state as it doesn't work after Angular migration

* ACS-8325 add formControl and tests

* ACS-8325 add condition to use title if no description provided and cover with test

* ACS-8325 add tooltip and update Badge interface

* ACS-8326 enable running actions from bulk dropdown and resetting dropdown selection

* ACS-8326 review remarks - use select control, fix naming

* ACS-8326 fix unit test selectors

* ACS-8326 review remarks - change property name to more universal, adjust unit tests

---------

Co-authored-by: Darya Balvanovich <darya.balvanovich@hyland.com>

* [ACS-8424][Bulk Legal Hold] Add Badge for items (#3985)

* [ACS-8424] display badges in search result

* [ACS-8424] display badges in search result

* ACS-8424 create separate badge component, add/move unit tests, undo highlight change failing tests

* ACS-8424 template cleanup

* ACS-8424 template cleanup cleanup

* ACS-8424 revert single deletion in template

* ACS-8424 rename new component, change property order

---------

Co-authored-by: g-jaskowski <grzegorz.jaskowski@hyland.com>

* ACS-8458 refactor styles (#4018)

* [ACS-8489] Legal Holds keyboard accessibility (#4009)

* ACS-8489 handle keyboard accessibility

* ACS-8489 wording fix

* ACS-8489 change function name

* ACS-8489 fix typo

* ACS-8489 review remarks - simplify keyboard event handling, improve unit tests

* ACS-8489 move duplicated code to method

* ACS-8489 change type name to more precise

* [ACS-8036] fix icon visibility in mat-select

* ACS-8036 fix styles and import

* ACS-8036 remove async from test

* ACS-8036 handle event when dropdown closed

* ACS-8036 fix rebase issues

* ACS-8036 fix rebase issues

* ACS-8036 fix rebase

---------

Co-authored-by: Grzegorz Jaśkowski <138671284+g-jaskowski@users.noreply.github.com>
Co-authored-by: tamaragruszka <156320606+tamaragruszka@users.noreply.github.com>
Co-authored-by: g-jaskowski <grzegorz.jaskowski@hyland.com>
Co-authored-by: tamaragruszka <tamara.gruszka@hyland.com>
This commit is contained in:
Darya Blavanovich
2024-08-14 20:15:19 +02:00
committed by GitHub
parent 068f6bb8e9
commit 53e90312b0
30 changed files with 856 additions and 340 deletions

View File

@@ -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,

View File

@@ -0,0 +1,42 @@
<mat-form-field
*ngIf="items?.length"
[title]="tooltip"
appearance="outline"
class="aca-bulk-actions-form-field"
data-automation-id="aca-bulk-actions-form-field"
>
<mat-select
[formControl]="bulkSelectControl"
[placeholder]="placeholder"
panelClass="aca-bulk-actions-select"
disableOptionCentering
data-automation-id="aca-bulk-actions-dropdown"
(keydown)="onKeyDown($event)"
>
<mat-select-trigger>
<adf-icon
*ngIf="bulkSelectControl.value?.icon"
[title]="bulkSelectControl.value?.title | translate"
[value]="bulkSelectControl.value?.icon"
[attr.data-automation-id]="'aca-bulk-action-icon-' + bulkSelectControl.value?.id"
></adf-icon>
{{ bulkSelectControl.value?.title | translate }}
</mat-select-trigger>
<mat-option
*ngFor="let option of items"
[value]="option"
[title]="option.tooltip | translate"
[attr.data-automation-id]="option.id"
(click)="runAction(option)"
>
<adf-icon
*ngIf="option.icon"
[title]="option.title | translate"
[value]="option.icon"
[attr.data-automation-id]="'aca-bulk-action-icon-' + option.id"
></adf-icon>
{{ option.title | translate }}
</mat-option>
</mat-select>
</mat-form-field>

View File

@@ -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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<BulkActionsDropdownComponent>;
let store: Store<AppStore>;
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<number> = 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();
});
});
});
});

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<number> = this.store.select(getSearchItemsTotalCount);
private readonly onDestroy$ = new Subject();
constructor(private store: Store<AppStore>, 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);
}
}
}

View File

@@ -0,0 +1,19 @@
<div class="aca-datatable-cell-badges-container">
<ng-container *ngFor="let badge of badges">
<adf-dynamic-component
*ngIf="badge.component; else iconBadge"
[id]="badge.component"
[data]="{ node }"
></adf-dynamic-component>
<ng-template #iconBadge>
<adf-icon
class="adf-datatable-cell-badge"
[title]="badge.tooltip | translate"
[value]="badge.icon"
(click)="onBadgeClick(badge)"
(keypress.enter)="onKeyPress(badge)"
tabindex="0"
></adf-icon>
</ng-template>
</ng-container>
</div>

View File

@@ -0,0 +1,7 @@
.aca-datatable-cell-badges-container {
display: flex;
.adf-datatable-cell-badge {
color: var(--theme-secondary-text);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<DatatableCellBadgesComponent>;
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);
});
});
});

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<boolean>();
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);
}
}

View File

@@ -19,12 +19,5 @@
<aca-locked-by [node]="context.row.node"></aca-locked-by>
</ng-container>
</div>
<div class="aca-name-column-badges">
<ng-container *ngFor="let badge of badges">
<adf-dynamic-component *ngIf="badge.component; else iconBadge" [id]="badge.component" [data]="{ node }"></adf-dynamic-component>
<ng-template #iconBadge>
<adf-icon class="adf-datatable-cell-badge" [title]="badge.tooltip | translate" [value]="badge.icon" (click)="onBadgeClick(badge)"></adf-icon>
</ng-template>
</ng-container>
</div>
<aca-datatable-cell-badges [node]="node"></aca-datatable-cell-badges>
</div>

View File

@@ -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);
}

View File

@@ -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<CustomNameColumnComponent>;
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);
});
});

View File

@@ -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);
}
}
}

View File

@@ -1,38 +1,41 @@
<div class="search-file-name">
<span
tabindex="0"
role="link"
*ngIf="isFile"
(click)="showPreview($event)"
(keyup.enter)="showPreview($event)"
class="aca-link aca-crop-text"
[title]="nameStripped"
[innerHTML]="name$ | async"
></span>
<span
tabindex="0"
role="link"
*ngIf="!isFile"
(click)="navigate($event)"
(keyup.enter)="navigate($event)"
class="bold aca-link aca-crop-text"
[title]="nameStripped"
[innerHTML]="name$ | async"
></span>
<span
data-automation-id="search-results-entry-title"
<div class="aca-search-results-text">
<div class="search-file-name">
<span
tabindex="0"
role="link"
*ngIf="isFile"
(click)="showPreview($event)"
(keyup.enter)="showPreview($event)"
class="aca-link aca-crop-text"
[title]="nameStripped"
[innerHTML]="name$ | async"
></span>
<span
tabindex="0"
role="link"
*ngIf="!isFile"
(click)="navigate($event)"
(keyup.enter)="navigate($event)"
class="bold aca-link aca-crop-text"
[title]="nameStripped"
[innerHTML]="name$ | async"
></span>
<span
data-automation-id="search-results-entry-title"
class="aca-crop-text"
[title]="titleStripped"
[innerHTML]="title$ | async"
></span>
</div>
<div
data-automation-id="search-results-entry-description"
class="aca-crop-text"
[title]="titleStripped"
[innerHTML]="title$ | async"
></span>
[title]="descriptionStripped"
[innerHTML]="description$ | async"
></div>
<div class="aca-result-location">
<aca-location-link [context]="context" [showLocation]="true"></aca-location-link>
</div>
<div class="aca-result-content aca-crop-text" [title]="contentStripped" [innerHTML]="content$ | async"></div>
</div>
<div
data-automation-id="search-results-entry-description"
class="aca-crop-text"
[title]="descriptionStripped"
[innerHTML]="description$ | async"
></div>
<div class="aca-result-location">
<aca-location-link [context]="context" [showLocation]="true"></aca-location-link>
</div>
<div class="aca-result-content aca-crop-text" [title]="contentStripped" [innerHTML]="content$ | async"></div>
<aca-datatable-cell-badges [node]="context.row.node"></aca-datatable-cell-badges>

View File

@@ -1,4 +1,7 @@
.aca-search-results-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
width: inherit;

View File

@@ -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);
});
});

View File

@@ -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(

View File

@@ -1,6 +1,7 @@
<aca-page-layout>
<div class="aca-page-layout-header">
<aca-search-input></aca-search-input>
<aca-bulk-actions-dropdown *ngIf="bulkActions" [items]="bulkActions"></aca-bulk-actions-dropdown>
<div class="aca-search-toolbar-spacer"></div>
<aca-toolbar [items]="actions"></aca-toolbar>
</div>

View File

@@ -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() {

View File

@@ -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 };
}