Merge pull request #1936 from atmire/w2p-95335_created-ListableNotificationObjectComponent_contribute-7.2

Created `ListableNotificationObject`
This commit is contained in:
Tim Donohue
2022-12-07 10:44:34 -06:00
committed by GitHub
12 changed files with 183 additions and 34 deletions

View File

@@ -21,6 +21,7 @@ const effects = [
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator // put only entry components that use custom decorator
NavbarSectionComponent, NavbarSectionComponent,
ExpandableNavbarSectionComponent,
ThemedExpandableNavbarSectionComponent, ThemedExpandableNavbarSectionComponent,
]; ];
@@ -34,11 +35,9 @@ const ENTRY_COMPONENTS = [
CoreModule.forRoot() CoreModule.forRoot()
], ],
declarations: [ declarations: [
...ENTRY_COMPONENTS,
NavbarComponent, NavbarComponent,
ThemedNavbarComponent, ThemedNavbarComponent,
NavbarSectionComponent,
ExpandableNavbarSectionComponent,
ThemedExpandableNavbarSectionComponent,
], ],
providers: [], providers: [],
exports: [ exports: [

View File

@@ -53,8 +53,9 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
* Perform a search for authorized collections with the current query and page * Perform a search for authorized collections with the current query and page
* @param query Query to search objects for * @param query Query to search objects for
* @param page Page to retrieve * @param page Page to retrieve
* @param useCache Whether or not to use the cache
*/ */
search(query: string, page: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> { search(query: string, page: number, useCache: boolean = true): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
let searchListService$: Observable<RemoteData<PaginatedList<Collection>>> = null; let searchListService$: Observable<RemoteData<PaginatedList<Collection>>> = null;
const findOptions: FindListOptions = { const findOptions: FindListOptions = {
currentPage: page, currentPage: page,
@@ -69,7 +70,7 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
findOptions); findOptions);
} else { } else {
searchListService$ = this.collectionDataService searchListService$ = this.collectionDataService
.getAuthorizedCollection(query, findOptions, true, false, followLink('parentCommunity')); .getAuthorizedCollection(query, findOptions, useCache, false, followLink('parentCommunity'));
} }
return searchListService$.pipe( return searchListService$.pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),

View File

@@ -21,12 +21,12 @@
</button> </button>
<button *ngFor="let listEntry of (listEntries$ | async)" <button *ngFor="let listEntry of (listEntries$ | async)"
class="list-group-item list-group-item-action border-0 list-entry" class="list-group-item list-group-item-action border-0 list-entry"
[ngClass]="{'bg-primary': listEntry.indexableObject.id === currentDSOId}" [ngClass]="{'bg-primary': listEntry['id'] === currentDSOId}"
title="{{ getName(listEntry) }}" title="{{ getName(listEntry) }}"
dsHoverClass="ds-hover" dsHoverClass="ds-hover"
(click)="onSelect.emit(listEntry.indexableObject)" #listEntryElement> (click)="onClick(listEntry)" #listEntryElement>
<ds-listable-object-component-loader [object]="listEntry" [viewMode]="viewMode" <ds-listable-object-component-loader [object]="listEntry" [viewMode]="viewMode"
[linkType]=linkTypes.None [context]="getContext(listEntry.indexableObject.id)"></ds-listable-object-component-loader> [linkType]=linkTypes.None [context]="getContext(listEntry['id'])"></ds-listable-object-component-loader>
</button> </button>
</ng-container> </ng-container>
<button *ngIf="loading" <button *ngIf="loading"

View File

@@ -35,6 +35,14 @@ import { RemoteData } from '../../../core/data/remote-data';
import { NotificationsService } from '../../notifications/notifications.service'; import { NotificationsService } from '../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import {
ListableNotificationObject
} from '../../object-list/listable-notification-object/listable-notification-object.model';
import { ListableObject } from '../../object-collection/shared/listable-object.model';
import { NotificationType } from '../../notifications/models/notification-type';
import {
LISTABLE_NOTIFICATION_OBJECT
} from '../../object-list/listable-notification-object/listable-notification-object.resource-type';
@Component({ @Component({
selector: 'ds-dso-selector', selector: 'ds-dso-selector',
@@ -82,7 +90,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
/** /**
* List with search results of DSpace objects for the current query * List with search results of DSpace objects for the current query
*/ */
listEntries$: BehaviorSubject<SearchResult<DSpaceObject>[]> = new BehaviorSubject(null); listEntries$: BehaviorSubject<ListableObject[]> = new BehaviorSubject(null);
/** /**
* The current page to load * The current page to load
@@ -116,11 +124,6 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
*/ */
linkTypes = CollectionElementLinkType; linkTypes = CollectionElementLinkType;
/**
* Track whether the element has the mouse over it
*/
isMouseOver = false;
/** /**
* Array to track all subscriptions and unsubscribe them onDestroy * Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array} * @type {Array}
@@ -182,24 +185,28 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
}) })
); );
}) })
).subscribe((rd) => { ).subscribe((rd: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>) => {
this.loading = false; this.updateList(rd);
if (rd.hasSucceeded) {
const currentEntries = this.listEntries$.getValue();
if (hasNoValue(currentEntries)) {
this.listEntries$.next(rd.payload.page);
} else {
this.listEntries$.next([...currentEntries, ...rd.payload.page]);
}
// Check if there are more pages available after the current one
this.hasNextPage = rd.payload.totalElements > this.listEntries$.getValue().length;
} else {
this.listEntries$.next(null);
this.hasNextPage = false;
}
})); }));
} }
updateList(rd: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>) {
this.loading = false;
const currentEntries = this.listEntries$.getValue();
if (rd.hasSucceeded) {
if (hasNoValue(currentEntries)) {
this.listEntries$.next(rd.payload.page);
} else {
this.listEntries$.next([...currentEntries, ...rd.payload.page]);
}
// Check if there are more pages available after the current one
this.hasNextPage = rd.payload.totalElements > this.listEntries$.getValue().length;
} else {
this.listEntries$.next([...(hasNoValue(currentEntries) ? [] : this.listEntries$.getValue()), new ListableNotificationObject(NotificationType.Error, 'dso-selector.results-could-not-be-retrieved', LISTABLE_NOTIFICATION_OBJECT.value)]);
this.hasNextPage = false;
}
}
/** /**
* Get a query to send for retrieving the current DSO * Get a query to send for retrieving the current DSO
*/ */
@@ -211,8 +218,9 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
* Perform a search for the current query and page * Perform a search for the current query and page
* @param query Query to search objects for * @param query Query to search objects for
* @param page Page to retrieve * @param page Page to retrieve
* @param useCache Whether or not to use the cache
*/ */
search(query: string, page: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> { search(query: string, page: number, useCache: boolean = true): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
return this.searchService.search( return this.searchService.search(
new PaginatedSearchOptions({ new PaginatedSearchOptions({
query: query, query: query,
@@ -220,7 +228,9 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
pagination: Object.assign({}, this.defaultPagination, { pagination: Object.assign({}, this.defaultPagination, {
currentPage: page currentPage: page
}) })
}) }),
null,
useCache,
).pipe( ).pipe(
getFirstCompletedRemoteData() getFirstCompletedRemoteData()
); );
@@ -262,7 +272,28 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
} }
getName(searchResult: SearchResult<DSpaceObject>): string { /**
return this.dsoNameService.getName(searchResult.indexableObject); * Handles the user clicks on the {@link ListableObject}s. When the {@link listableObject} is a
* {@link ListableObject} it will retry the error when the user clicks it. Otherwise it will emit the {@link onSelect}.
*
* @param listableObject The {@link ListableObject} to evaluate
*/
onClick(listableObject: ListableObject): void {
if (hasValue((listableObject as SearchResult<DSpaceObject>).indexableObject)) {
this.onSelect.emit((listableObject as SearchResult<DSpaceObject>).indexableObject);
} else {
this.listEntries$.value.pop();
this.hasNextPage = true;
this.search(this.input.value ? this.input.value : '', this.currentPage$.value, false).pipe(
getFirstCompletedRemoteData(),
).subscribe((rd: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>) => {
this.updateList(rd);
});
}
}
getName(listableObject: ListableObject): string {
return hasValue((listableObject as SearchResult<DSpaceObject>).indexableObject) ?
this.dsoNameService.getName((listableObject as SearchResult<DSpaceObject>).indexableObject) : null;
} }
} }

View File

@@ -0,0 +1 @@
<div class="alert d-block {{ object?.notificationType }} m-0">{{ object?.message | translate }}</div>

View File

@@ -0,0 +1,43 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ListableNotificationObjectComponent } from './listable-notification-object.component';
import { NotificationType } from '../../notifications/models/notification-type';
import { ListableNotificationObject } from './listable-notification-object.model';
import { By } from '@angular/platform-browser';
import { TranslateModule } from '@ngx-translate/core';
describe('ListableNotificationObjectComponent', () => {
let component: ListableNotificationObjectComponent;
let fixture: ComponentFixture<ListableNotificationObjectComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
],
declarations: [
ListableNotificationObjectComponent,
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ListableNotificationObjectComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
describe('ui', () => {
it('should display the given error message', () => {
component.object = new ListableNotificationObject(NotificationType.Error, 'test error message');
fixture.detectChanges();
const listableNotificationObject: Element = fixture.debugElement.query(By.css('.alert')).nativeElement;
expect(listableNotificationObject.className).toContain(NotificationType.Error);
expect(listableNotificationObject.innerHTML).toBe('test error message');
});
});
afterEach(() => {
fixture.debugElement.nativeElement.remove();
});
});

View File

@@ -0,0 +1,21 @@
import { Component } from '@angular/core';
import {
AbstractListableElementComponent
} from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { ListableNotificationObject } from './listable-notification-object.model';
import { listableObjectComponent } from '../../object-collection/shared/listable-object/listable-object.decorator';
import { ViewMode } from '../../../core/shared/view-mode.model';
import { LISTABLE_NOTIFICATION_OBJECT } from './listable-notification-object.resource-type';
/**
* The component for displaying a notifications inside an object list
*/
@listableObjectComponent(ListableNotificationObject, ViewMode.ListElement)
@listableObjectComponent(LISTABLE_NOTIFICATION_OBJECT.value, ViewMode.ListElement)
@Component({
selector: 'ds-listable-notification-object',
templateUrl: './listable-notification-object.component.html',
styleUrls: ['./listable-notification-object.component.scss'],
})
export class ListableNotificationObjectComponent extends AbstractListableElementComponent<ListableNotificationObject> {
}

View File

@@ -0,0 +1,36 @@
import { ListableObject } from '../../object-collection/shared/listable-object.model';
import { typedObject } from '../../../core/cache/builders/build-decorators';
import { TypedObject } from '../../../core/cache/typed-object.model';
import { LISTABLE_NOTIFICATION_OBJECT } from './listable-notification-object.resource-type';
import { GenericConstructor } from '../../../core/shared/generic-constructor';
import { NotificationType } from '../../notifications/models/notification-type';
import { ResourceType } from '../../../core/shared/resource-type';
/**
* Object representing a notification message inside a list of objects
*/
@typedObject
export class ListableNotificationObject extends ListableObject implements TypedObject {
static type: ResourceType = LISTABLE_NOTIFICATION_OBJECT;
type: ResourceType = LISTABLE_NOTIFICATION_OBJECT;
protected renderTypes: string[];
constructor(
public notificationType: NotificationType = NotificationType.Error,
public message: string = 'listable-notification-object.default-message',
...renderTypes: string[]
) {
super();
this.renderTypes = renderTypes;
}
/**
* Method that returns as which type of object this object should be rendered.
*/
getRenderTypes(): (string | GenericConstructor<ListableObject>)[] {
return [...this.renderTypes, this.constructor as GenericConstructor<ListableObject>];
}
}

View File

@@ -0,0 +1,9 @@
import { ResourceType } from '../../../core/shared/resource-type';
/**
* The resource type for {@link ListableNotificationObject}
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const LISTABLE_NOTIFICATION_OBJECT = new ResourceType('listable-notification-object');

View File

@@ -323,6 +323,9 @@ import {
} from '../item-page/simple/field-components/specific-field/title/item-page-title-field.component'; } from '../item-page/simple/field-components/specific-field/title/item-page-title-field.component';
import { MarkdownPipe } from './utils/markdown.pipe'; import { MarkdownPipe } from './utils/markdown.pipe';
import { GoogleRecaptchaModule } from '../core/google-recaptcha/google-recaptcha.module'; import { GoogleRecaptchaModule } from '../core/google-recaptcha/google-recaptcha.module';
import {
ListableNotificationObjectComponent
} from './object-list/listable-notification-object/listable-notification-object.component';
const MODULES = [ const MODULES = [
CommonModule, CommonModule,
@@ -510,6 +513,7 @@ const COMPONENTS = [
ScopeSelectorModalComponent, ScopeSelectorModalComponent,
ItemPageTitleFieldComponent, ItemPageTitleFieldComponent,
ThemedSearchNavbarComponent, ThemedSearchNavbarComponent,
ListableNotificationObjectComponent,
]; ];
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
@@ -575,7 +579,8 @@ const ENTRY_COMPONENTS = [
OnClickMenuItemComponent, OnClickMenuItemComponent,
TextMenuItemComponent, TextMenuItemComponent,
ScopeSelectorModalComponent, ScopeSelectorModalComponent,
ExternalLinkMenuItemComponent ExternalLinkMenuItemComponent,
ListableNotificationObjectComponent,
]; ];
const SHARED_ITEM_PAGE_COMPONENTS = [ const SHARED_ITEM_PAGE_COMPONENTS = [

View File

@@ -1410,6 +1410,8 @@
"dso-selector.claim.item.create-from-scratch": "Create a new one", "dso-selector.claim.item.create-from-scratch": "Create a new one",
"dso-selector.results-could-not-be-retrieved": "Something went wrong, please refresh again ↻",
"confirmation-modal.export-metadata.header": "Export metadata for {{ dsoName }}", "confirmation-modal.export-metadata.header": "Export metadata for {{ dsoName }}",
"confirmation-modal.export-metadata.info": "Are you sure you want to export metadata for {{ dsoName }}", "confirmation-modal.export-metadata.info": "Are you sure you want to export metadata for {{ dsoName }}",
@@ -4826,4 +4828,5 @@
"person.orcid.registry.auth": "ORCID Authorizations", "person.orcid.registry.auth": "ORCID Authorizations",
"home.recent-submissions.head": "Recent Submissions", "home.recent-submissions.head": "Recent Submissions",
"listable-notification-object.default-message": "This object couldn't be retrieved",
} }