75058: multiple badges fix, item alerts, tests

This commit is contained in:
Kristof De Langhe
2020-12-08 13:07:18 +01:00
parent d50dd12d3d
commit fa36b3518d
15 changed files with 236 additions and 123 deletions

View File

@@ -1,6 +1,6 @@
<ng-template dsListableObject> <ng-template dsListableObject>
</ng-template> </ng-template>
<div #badges class="position-absolute ml-1"> <div #badges>
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>
<ul #buttons class="list-group list-group-flush"> <ul #buttons class="list-group list-group-flush">

View File

@@ -1,5 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@@ -13,7 +12,6 @@ import { SharedModule } from '../../../../../shared/shared.module';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-result-grid-element.component'; import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-result-grid-element.component';
@@ -71,51 +69,4 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
describe('when the item is not withdrawn', () => {
beforeEach(() => {
component.dso.isWithdrawn = false;
fixture.detectChanges();
});
it('should not show the withdrawn badge', () => {
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
expect(badge).toBeNull();
});
});
describe('when the item is withdrawn', () => {
beforeEach(() => {
component.dso.isWithdrawn = true;
fixture.detectChanges();
});
it('should show the withdrawn badge', () => {
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
expect(badge).not.toBeNull();
});
});
describe('when the item is not private', () => {
beforeEach(() => {
component.dso.isDiscoverable = true;
fixture.detectChanges();
});
it('should not show the private badge', () => {
const badge = fixture.debugElement.query(By.css('div.private-badge'));
expect(badge).toBeNull();
});
});
describe('when the item is private', () => {
beforeEach(() => {
component.dso.isDiscoverable = false;
fixture.detectChanges();
});
it('should show the private badge', () => {
const badge = fixture.debugElement.query(By.css('div.private-badge'));
expect(badge).not.toBeNull();
});
})
}); });

View File

@@ -1,12 +1,7 @@
<div *ngIf="dso && !dso.isDiscoverable" class="private-badge">
<span class="badge badge-danger">{{ "admin.search.item.private" | translate }}</span>
</div>
<div *ngIf="dso && dso.isWithdrawn" class="withdrawn-badge">
<span class="badge badge-warning">{{ "admin.search.item.withdrawn" | translate }}</span>
</div>
<ds-listable-object-component-loader [object]="object" <ds-listable-object-component-loader [object]="object"
[viewMode]="viewModes.ListElement" [viewMode]="viewModes.ListElement"
[index]="index" [index]="index"
[linkType]="linkType" [linkType]="linkType"
[listID]="listID"></ds-listable-object-component-loader> [listID]="listID"
[hideBadges]="true"></ds-listable-object-component-loader>
<ds-item-admin-search-result-actions-element [item]="dso" [small]="false"></ds-item-admin-search-result-actions-element> <ds-item-admin-search-result-actions-element [item]="dso" [small]="false"></ds-item-admin-search-result-actions-element>

View File

@@ -1,11 +1,9 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
import { ItemAdminSearchResultListElementComponent } from './item-admin-search-result-list-element.component'; import { ItemAdminSearchResultListElementComponent } from './item-admin-search-result-list-element.component';
@@ -51,51 +49,4 @@ describe('ItemAdminSearchResultListElementComponent', () => {
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
describe('when the item is not withdrawn', () => {
beforeEach(() => {
component.dso.isWithdrawn = false;
fixture.detectChanges();
});
it('should not show the withdrawn badge', () => {
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
expect(badge).toBeNull();
});
});
describe('when the item is withdrawn', () => {
beforeEach(() => {
component.dso.isWithdrawn = true;
fixture.detectChanges();
});
it('should show the withdrawn badge', () => {
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
expect(badge).not.toBeNull();
});
});
describe('when the item is not private', () => {
beforeEach(() => {
component.dso.isDiscoverable = true;
fixture.detectChanges();
});
it('should not show the private badge', () => {
const badge = fixture.debugElement.query(By.css('div.private-badge'));
expect(badge).toBeNull();
});
});
describe('when the item is private', () => {
beforeEach(() => {
component.dso.isDiscoverable = false;
fixture.detectChanges();
});
it('should show the private badge', () => {
const badge = fixture.debugElement.query(By.css('div.private-badge'));
expect(badge).not.toBeNull();
});
})
}); });

View File

@@ -1,6 +1,7 @@
<div class="container" *ngVar="(itemRD$ | async) as itemRD"> <div class="container" *ngVar="(itemRD$ | async) as itemRD">
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut> <div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
<div *ngIf="itemRD?.payload as item"> <div *ngIf="itemRD?.payload as item">
<ds-item-alerts [item]="item"></ds-item-alerts>
<ds-item-versions-notice [item]="item"></ds-item-versions-notice> <ds-item-versions-notice [item]="item"></ds-item-versions-notice>
<ds-view-tracker [object]="item"></ds-view-tracker> <ds-view-tracker [object]="item"></ds-view-tracker>
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader> <ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>

View File

@@ -0,0 +1,8 @@
<div>
<div *ngIf="item && !item.isDiscoverable" class="private-warning">
<ds-alert [type]="AlertTypeEnum.Warning" [content]="'item.alerts.private' | translate"></ds-alert>
</div>
<div *ngIf="item && item.isWithdrawn" class="withdrawn-warning">
<ds-alert [type]="AlertTypeEnum.Warning" [content]="'item.alerts.withdrawn' | translate"></ds-alert>
</div>
</div>

View File

@@ -0,0 +1,87 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ItemAlertsComponent } from './item-alerts.component';
import { TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Item } from '../../../core/shared/item.model';
import { By } from '@angular/platform-browser';
describe('ItemAlertsComponent', () => {
let component: ItemAlertsComponent;
let fixture: ComponentFixture<ItemAlertsComponent>;
let item: Item;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ItemAlertsComponent],
imports: [TranslateModule.forRoot()],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ItemAlertsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
describe('when the item is discoverable', () => {
beforeEach(() => {
item = Object.assign(new Item(), {
isDiscoverable: true
});
component.item = item;
fixture.detectChanges();
});
it('should not display the private alert', () => {
const privateWarning = fixture.debugElement.query(By.css('.private-warning'));
expect(privateWarning).toBeNull();
});
});
describe('when the item is not discoverable', () => {
beforeEach(() => {
item = Object.assign(new Item(), {
isDiscoverable: false
});
component.item = item;
fixture.detectChanges();
});
it('should display the private alert', () => {
const privateWarning = fixture.debugElement.query(By.css('.private-warning'));
expect(privateWarning).not.toBeNull();
});
});
describe('when the item is withdrawn', () => {
beforeEach(() => {
item = Object.assign(new Item(), {
isWithdrawn: true
});
component.item = item;
fixture.detectChanges();
});
it('should display the withdrawn alert', () => {
const privateWarning = fixture.debugElement.query(By.css('.withdrawn-warning'));
expect(privateWarning).not.toBeNull();
});
});
describe('when the item is not withdrawn', () => {
beforeEach(() => {
item = Object.assign(new Item(), {
isWithdrawn: false
});
component.item = item;
fixture.detectChanges();
});
it('should not display the withdrawn alert', () => {
const privateWarning = fixture.debugElement.query(By.css('.withdrawn-warning'));
expect(privateWarning).toBeNull();
});
});
});

View File

@@ -0,0 +1,24 @@
import { Component, Input } from '@angular/core';
import { Item } from '../../../core/shared/item.model';
import { AlertType } from '../../alert/aletr-type';
@Component({
selector: 'ds-item-alerts',
templateUrl: './item-alerts.component.html',
styleUrls: ['./item-alerts.component.scss']
})
/**
* Component displaying alerts for an item
*/
export class ItemAlertsComponent {
/**
* The Item to display alerts for
*/
@Input() item: Item;
/**
* The AlertType enumeration
* @type {AlertType}
*/
public AlertTypeEnum = AlertType;
}

View File

@@ -1,9 +1,9 @@
<div #badges> <div [ngClass]="{'d-none' : hideBadges}" #badges>
<div *ngIf="objectAsAny && !objectAsAny.isDiscoverable" class="private-badge"> <div *ngIf="privateBadge" class="private-badge">
<span class="badge badge-danger">{{ "admin.search.item.private" | translate }}</span> <span class="badge badge-danger">{{ "item.badge.private" | translate }}</span>
</div> </div>
<div *ngIf="objectAsAny && objectAsAny.isWithdrawn" class="withdrawn-badge"> <div *ngIf="withdrawnBadge" class="withdrawn-badge">
<span class="badge badge-warning">{{ "admin.search.item.withdrawn" | translate }}</span> <span class="badge badge-warning">{{ "item.badge.withdrawn" | translate }}</span>
</div> </div>
</div> </div>
<ng-template dsListableObject></ng-template> <ng-template dsListableObject></ng-template>

View File

@@ -9,6 +9,9 @@ import * as listableObjectDecorators from './listable-object.decorator';
import { PublicationListElementComponent } from '../../../object-list/item-list-element/item-types/publication/publication-list-element.component'; import { PublicationListElementComponent } from '../../../object-list/item-list-element/item-types/publication/publication-list-element.component';
import { ListableObjectDirective } from './listable-object.directive'; import { ListableObjectDirective } from './listable-object.directive';
import { spyOnExported } from '../../../testing/utils.test'; import { spyOnExported } from '../../../testing/utils.test';
import { TranslateModule } from '@ngx-translate/core';
import { By } from '@angular/platform-browser';
import { Item } from '../../../../core/shared/item.model';
const testType = 'TestType'; const testType = 'TestType';
const testContext = Context.Search; const testContext = Context.Search;
@@ -26,7 +29,7 @@ describe('ListableObjectComponentLoaderComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [TranslateModule.forRoot()],
declarations: [ListableObjectComponentLoaderComponent, PublicationListElementComponent, ListableObjectDirective], declarations: [ListableObjectComponentLoaderComponent, PublicationListElementComponent, ListableObjectDirective],
schemas: [NO_ERRORS_SCHEMA], schemas: [NO_ERRORS_SCHEMA],
providers: [ComponentFactoryResolver] providers: [ComponentFactoryResolver]
@@ -55,4 +58,63 @@ describe('ListableObjectComponentLoaderComponent', () => {
expect(listableObjectDecorators.getListableObjectComponent).toHaveBeenCalledWith([testType], testViewMode, testContext); expect(listableObjectDecorators.getListableObjectComponent).toHaveBeenCalledWith([testType], testViewMode, testContext);
}) })
}); });
describe('when the object is an item and viewMode is a list', () => {
beforeEach(() => {
comp.object = Object.assign(new Item());
comp.viewMode = ViewMode.ListElement;
});
describe('when the item is not withdrawn', () => {
beforeEach(() => {
(comp.object as any).isWithdrawn = false;
comp.initBadges();
fixture.detectChanges();
});
it('should not show the withdrawn badge', () => {
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
expect(badge).toBeNull();
});
});
describe('when the item is withdrawn', () => {
beforeEach(() => {
(comp.object as any).isWithdrawn = true;
comp.initBadges();
fixture.detectChanges();
});
it('should show the withdrawn badge', () => {
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
expect(badge).not.toBeNull();
});
});
describe('when the item is not private', () => {
beforeEach(() => {
(comp.object as any).isDiscoverable = true;
comp.initBadges();
fixture.detectChanges();
});
it('should not show the private badge', () => {
const badge = fixture.debugElement.query(By.css('div.private-badge'));
expect(badge).toBeNull();
});
});
describe('when the item is private', () => {
beforeEach(() => {
(comp.object as any).isDiscoverable = false;
comp.initBadges();
fixture.detectChanges();
});
it('should show the private badge', () => {
const badge = fixture.debugElement.query(By.css('div.private-badge'));
expect(badge).not.toBeNull();
});
});
});
}); });

View File

@@ -6,6 +6,7 @@ import { getListableObjectComponent } from './listable-object.decorator';
import { GenericConstructor } from '../../../../core/shared/generic-constructor'; import { GenericConstructor } from '../../../../core/shared/generic-constructor';
import { ListableObjectDirective } from './listable-object.directive'; import { ListableObjectDirective } from './listable-object.directive';
import { CollectionElementLinkType } from '../../collection-element-link.type'; import { CollectionElementLinkType } from '../../collection-element-link.type';
import { hasValue } from '../../../empty.util';
@Component({ @Component({
selector: 'ds-listable-object-component-loader', selector: 'ds-listable-object-component-loader',
@@ -56,6 +57,11 @@ export class ListableObjectComponentLoaderComponent implements OnInit {
*/ */
@Input() value: string; @Input() value: string;
/**
* Whether or not informational badges (e.g. Private, Withdrawn) should be hidden
*/
@Input() hideBadges = false;
/** /**
* Directive hook used to place the dynamic child component * Directive hook used to place the dynamic child component
*/ */
@@ -68,11 +74,14 @@ export class ListableObjectComponentLoaderComponent implements OnInit {
@ViewChild('badges', { static: true }) badges: ElementRef; @ViewChild('badges', { static: true }) badges: ElementRef;
/** /**
* The provided object as any * Whether or not the "Private" badge should be displayed for this listable object
* This is required to access the object's "isDiscoverable" and "isWithdrawn" properties from the template without
* knowing the object's type
*/ */
objectAsAny: any; privateBadge = false;
/**
* Whether or not the "Withdrawn" badge should be displayed for this listable object
*/
withdrawnBadge = false;
constructor(private componentFactoryResolver: ComponentFactoryResolver) { constructor(private componentFactoryResolver: ComponentFactoryResolver) {
} }
@@ -81,7 +90,7 @@ export class ListableObjectComponentLoaderComponent implements OnInit {
* Setup the dynamic child component * Setup the dynamic child component
*/ */
ngOnInit(): void { ngOnInit(): void {
this.objectAsAny = this.object as any; this.initBadges();
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent()); const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent());
@@ -105,6 +114,19 @@ export class ListableObjectComponentLoaderComponent implements OnInit {
(componentRef.instance as any).value = this.value; (componentRef.instance as any).value = this.value;
} }
/**
* Initialize which badges should be visible in the listable component
*/
initBadges() {
let objectAsAny = this.object as any;
if (hasValue(objectAsAny.indexableObject)) {
objectAsAny = objectAsAny.indexableObject;
}
const objectExistsAndValidViewMode = hasValue(objectAsAny) && this.viewMode !== ViewMode.StandalonePage;
this.privateBadge = objectExistsAndValidViewMode && hasValue(objectAsAny.isDiscoverable) && !objectAsAny.isDiscoverable;
this.withdrawnBadge = objectExistsAndValidViewMode && hasValue(objectAsAny.isWithdrawn) && objectAsAny.isWithdrawn;
}
/** /**
* Fetch the component depending on the item's relationship type, view mode and context * Fetch the component depending on the item's relationship type, view mode and context
* @returns {GenericConstructor<Component>} * @returns {GenericConstructor<Component>}

View File

@@ -1,6 +1,8 @@
<div class="card" [@focusShadow]="(isCollapsed$ | async)?'blur':'focus'"> <div class="card" [@focusShadow]="(isCollapsed$ | async)?'blur':'focus'">
<ds-truncatable [id]="dso.id"> <ds-truncatable [id]="dso.id">
<ng-content></ng-content> <div class="position-absolute ml-1">
<ng-content></ng-content>
</div>
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/items/' + dso.id]" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/items/' + dso.id]"
class="card-img-top full-width"> class="card-img-top full-width">
<div> <div>

View File

@@ -218,6 +218,7 @@ import { AuthorizedCollectionSelectorComponent } from './dso-selector/dso-select
import { DsoPageEditButtonComponent } from './dso-page/dso-page-edit-button/dso-page-edit-button.component'; import { DsoPageEditButtonComponent } from './dso-page/dso-page-edit-button/dso-page-edit-button.component';
import { HoverClassDirective } from './hover-class.directive'; import { HoverClassDirective } from './hover-class.directive';
import { ValidationSuggestionsComponent } from './input-suggestions/validation-suggestions/validation-suggestions.component'; import { ValidationSuggestionsComponent } from './input-suggestions/validation-suggestions/validation-suggestions.component';
import { ItemAlertsComponent } from './item/item-alerts/item-alerts.component';
const MODULES = [ const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here // Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -504,7 +505,8 @@ const ENTRY_COMPONENTS = [
const SHARED_ITEM_PAGE_COMPONENTS = [ const SHARED_ITEM_PAGE_COMPONENTS = [
MetadataFieldWrapperComponent, MetadataFieldWrapperComponent,
MetadataValuesComponent, MetadataValuesComponent,
DsoPageEditButtonComponent DsoPageEditButtonComponent,
ItemAlertsComponent,
]; ];
const PROVIDERS = [ const PROVIDERS = [

View File

@@ -462,14 +462,10 @@
"admin.search.item.move": "Move", "admin.search.item.move": "Move",
"admin.search.item.private": "Private",
"admin.search.item.reinstate": "Reinstate", "admin.search.item.reinstate": "Reinstate",
"admin.search.item.withdraw": "Withdraw", "admin.search.item.withdraw": "Withdraw",
"admin.search.item.withdrawn": "Withdrawn",
"admin.search.title": "Administrative Search", "admin.search.title": "Administrative Search",
@@ -1329,12 +1325,24 @@
"item.alerts.private": "This item is private",
"item.alerts.withdrawn": "This item has been withdrawn",
"item.edit.authorizations.heading": "With this editor you can view and alter the policies of an item, plus alter policies of individual item components: bundles and bitstreams. Briefly, an item is a container of bundles, and bundles are containers of bitstreams. Containers usually have ADD/REMOVE/READ/WRITE policies, while bitstreams only have READ/WRITE policies.", "item.edit.authorizations.heading": "With this editor you can view and alter the policies of an item, plus alter policies of individual item components: bundles and bitstreams. Briefly, an item is a container of bundles, and bundles are containers of bitstreams. Containers usually have ADD/REMOVE/READ/WRITE policies, while bitstreams only have READ/WRITE policies.",
"item.edit.authorizations.title": "Edit item's Policies", "item.edit.authorizations.title": "Edit item's Policies",
"item.badge.private": "Private",
"item.badge.withdrawn": "Private",
"item.bitstreams.upload.bundle": "Bundle", "item.bitstreams.upload.bundle": "Bundle",
"item.bitstreams.upload.bundle.placeholder": "Select a bundle", "item.bitstreams.upload.bundle.placeholder": "Select a bundle",