mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
add modal, refine table,refactor, fix lint
This commit is contained in:
@@ -12,6 +12,7 @@ import { AdminNotifySearchResultComponent } from './admin-notify-search-result/a
|
||||
import {
|
||||
AdminNotifyOutgoingComponent
|
||||
} from './admin-notify-logs/admin-notify-outgoing/admin-notify-outgoing.component';
|
||||
import { AdminNotifyDetailModalComponent } from './admin-notify-detail-modal/admin-notify-detail-modal.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@@ -28,7 +29,8 @@ import {
|
||||
AdminNotifyMetricsComponent,
|
||||
AdminNotifyIncomingComponent,
|
||||
AdminNotifyOutgoingComponent,
|
||||
AdminNotifySearchResultComponent
|
||||
AdminNotifySearchResultComponent,
|
||||
AdminNotifyDetailModalComponent
|
||||
]
|
||||
})
|
||||
export class AdminNotifyDashboardModule {
|
||||
|
@@ -0,0 +1,14 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Message Detail</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="d-flex modal-body flex-column p-4">
|
||||
<div *ngFor="let key of notifyMessageKeys">
|
||||
<div class="d-flex w-100 justify-content-between mb-4">
|
||||
<div class="font-weight-bold mr-5">{{ key }}</div>
|
||||
<div class="text-nowrap text-truncate">{{ notifyMessage[key] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminNotifyDetailModalComponent } from './admin-notify-detail-modal.component';
|
||||
|
||||
describe('AdminNotifyDetailModalComponent', () => {
|
||||
let component: AdminNotifyDetailModalComponent;
|
||||
let fixture: ComponentFixture<AdminNotifyDetailModalComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ AdminNotifyDetailModalComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminNotifyDetailModalComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,32 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { AdminNotifyMessage } from '../models/admin-notify-message.model';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-admin-notify-detail-modal',
|
||||
templateUrl: './admin-notify-detail-modal.component.html',
|
||||
styleUrls: ['./admin-notify-detail-modal.component.scss']
|
||||
})
|
||||
export class AdminNotifyDetailModalComponent {
|
||||
@Input() notifyMessage: AdminNotifyMessage;
|
||||
@Input() notifyMessageKeys: string[];
|
||||
|
||||
/**
|
||||
* An event fired when the modal is closed
|
||||
*/
|
||||
@Output()
|
||||
response = new EventEmitter<boolean>();
|
||||
|
||||
|
||||
constructor(protected activeModal: NgbActiveModal) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Close the modal and set the response to true so RootComponent knows the modal was closed
|
||||
*/
|
||||
closeModal() {
|
||||
this.activeModal.close();
|
||||
this.response.emit(true);
|
||||
}
|
||||
}
|
@@ -1,22 +1,22 @@
|
||||
<div class="w-100">
|
||||
<div class="table-responsive">
|
||||
<table id="formats" class="table table-striped table-hover">
|
||||
<div class="table-responsive mt-2">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Timestamp</th>
|
||||
<th scope="col">Source</th>
|
||||
<th scope="col">Origin</th>
|
||||
<th scope="col">Target</th>
|
||||
<th scope="col">Type</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let message of notifyMessages">
|
||||
<td>
|
||||
<div>{{message.queueTimeout}}</div>
|
||||
<div class="text-nowrap">{{message.queueTimeout}}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{message.source}}</div>
|
||||
<div>{{message.origin}}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{message.target}}</div>
|
||||
@@ -27,8 +27,13 @@
|
||||
<td>
|
||||
<div>{{message.queueStatusLabel}}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column">
|
||||
<button class="btn mb-2 btn-dark" (click)="openDetailModal(message)">Detail</button>
|
||||
<button class="btn btn-warning">Reprocess</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { AdminNotifySearchResult } from '../models/admin-notify-message-search-result.model';
|
||||
import { ViewMode } from '../../../core/shared/view-mode.model';
|
||||
import { Context } from '../../../core/shared/context.model';
|
||||
@@ -10,16 +10,60 @@ import {
|
||||
TabulatableResultListElementsComponent
|
||||
} from '../../../shared/object-list/search-result-list-element/tabulatable-search-result/tabulatable-result-list-elements.component';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TruncatableService } from '../../../shared/truncatable/truncatable.service';
|
||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||
import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface';
|
||||
import { AdminNotifyDetailModalComponent } from '../admin-notify-detail-modal/admin-notify-detail-modal.component';
|
||||
|
||||
@tabulatableObjectsComponent(AdminNotifySearchResult, ViewMode.Table, Context.CoarNotify)
|
||||
@tabulatableObjectsComponent(PaginatedList<AdminNotifySearchResult>, ViewMode.Table, Context.CoarNotify)
|
||||
@Component({
|
||||
selector: 'ds-admin-notify-search-result',
|
||||
templateUrl: './admin-notify-search-result.component.html',
|
||||
styleUrls: ['./admin-notify-search-result.component.scss']
|
||||
})
|
||||
export class AdminNotifySearchResultComponent extends TabulatableResultListElementsComponent<PaginatedList<AdminNotifyMessage>, AdminNotifyMessage> implements OnInit{
|
||||
export class AdminNotifySearchResultComponent extends TabulatableResultListElementsComponent<PaginatedList<AdminNotifySearchResult>, AdminNotifySearchResult> implements OnInit{
|
||||
public notifyMessages: AdminNotifyMessage[];
|
||||
ngOnInit() {
|
||||
this.notifyMessages = this.objects.page.map(object => object.indexableObject);
|
||||
|
||||
private queueStatusMap = {
|
||||
QUEUE_STATUS_PROCESSED: 'Processed',
|
||||
QUEUE_STATUS_FAILED: 'Failed',
|
||||
QUEUE_STATUS_UNMAPPED_ACTION: 'Unmapped action',
|
||||
};
|
||||
|
||||
constructor(private modalService: NgbModal,
|
||||
protected truncatableService: TruncatableService,
|
||||
public dsoNameService: DSONameService,
|
||||
@Inject(APP_CONFIG) protected appConfig?: AppConfig) {
|
||||
super(truncatableService, dsoNameService, appConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map messages on init for readable representation
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.notifyMessages = this.objects.page.map(object => {
|
||||
const indexableObject = object.indexableObject;
|
||||
indexableObject.coarNotifyType = indexableObject.coarNotifyType.split(':')[1];
|
||||
indexableObject.queueStatusLabel = this.queueStatusMap[indexableObject.queueStatusLabel];
|
||||
return indexableObject;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open modal for details visualization
|
||||
* @param message the message to be displayed
|
||||
*/
|
||||
openDetailModal(message: AdminNotifyMessage) {
|
||||
const modalRef = this.modalService.open(AdminNotifyDetailModalComponent);
|
||||
const messageKeys = Object.keys(message);
|
||||
const keysToRead = [];
|
||||
messageKeys.forEach((key) => {
|
||||
if (typeof message[key] !== 'object') {
|
||||
keysToRead.push(key);
|
||||
}
|
||||
});
|
||||
modalRef.componentInstance.notifyMessage = message;
|
||||
modalRef.componentInstance.notifyMessageKeys = keysToRead;
|
||||
}
|
||||
}
|
||||
|
@@ -26,11 +26,35 @@ export class AdminNotifyMessage extends DSpaceObject {
|
||||
@autoserialize
|
||||
coarNotifyType: string;
|
||||
|
||||
/**
|
||||
* The type of the activity
|
||||
*/
|
||||
@autoserialize
|
||||
activityStreamType: string;
|
||||
|
||||
/**
|
||||
* The object the message reply to
|
||||
*/
|
||||
@autoserialize
|
||||
inReplyTo: string;
|
||||
|
||||
/**
|
||||
* The attempts of the queue
|
||||
*/
|
||||
@autoserialize
|
||||
queueAttempts: number;
|
||||
|
||||
/**
|
||||
* Timestamp of the last queue attempt
|
||||
*/
|
||||
@autoserialize
|
||||
queueLastStartTime: string;
|
||||
|
||||
/**
|
||||
* The type of the activity stream
|
||||
*/
|
||||
@autoserialize
|
||||
source: number;
|
||||
origin: number;
|
||||
|
||||
/**
|
||||
* The type of the activity stream
|
||||
@@ -56,11 +80,6 @@ export class AdminNotifyMessage extends DSpaceObject {
|
||||
@autoserialize
|
||||
queueStatus: number;
|
||||
|
||||
/**
|
||||
* The status of the queue
|
||||
*/
|
||||
@autoserialize
|
||||
indexableObject: AdminNotifyMessage;
|
||||
|
||||
@deserialize
|
||||
_links: {
|
||||
|
@@ -58,20 +58,19 @@
|
||||
</ds-object-detail>
|
||||
|
||||
|
||||
<ds-object-table
|
||||
[config]="config"
|
||||
[sortConfig]="sortConfig"
|
||||
[objects]="objects"
|
||||
[hideGear]="hideGear"
|
||||
[linkType]="linkType"
|
||||
[context]="context"
|
||||
[hidePaginationDetail]="hidePaginationDetail"
|
||||
[showPaginator]="showPaginator"
|
||||
[showThumbnails]="showThumbnails"
|
||||
(paginationChange)="onPaginationChange($event)"
|
||||
(pageChange)="onPageChange($event)"
|
||||
(pageSizeChange)="onPageSizeChange($event)"
|
||||
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||
(sortFieldChange)="onSortFieldChange($event)"
|
||||
*ngIf="(currentMode$ | async) === viewModeEnum.Table">
|
||||
<ds-object-table [config]="config"
|
||||
[sortConfig]="sortConfig"
|
||||
[objects]="objects"
|
||||
[hideGear]="hideGear"
|
||||
[linkType]="linkType"
|
||||
[context]="context"
|
||||
[hidePaginationDetail]="hidePaginationDetail"
|
||||
[showPaginator]="showPaginator"
|
||||
[showThumbnails]="showThumbnails"
|
||||
(paginationChange)="onPaginationChange($event)"
|
||||
(pageChange)="onPageChange($event)"
|
||||
(pageSizeChange)="onPageSizeChange($event)"
|
||||
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||
(sortFieldChange)="onSortFieldChange($event)"
|
||||
*ngIf="(currentMode$ | async) === viewModeEnum.Table">
|
||||
</ds-object-table>
|
||||
|
@@ -34,7 +34,7 @@ export const DEFAULT_THEME = '*';
|
||||
* - { level: 1, relevancy: 1 } is less relevant than { level: 2, relevancy: 0 }
|
||||
* - { level: 1, relevancy: 1 } is more relevant than null
|
||||
*/
|
||||
class MatchRelevancy {
|
||||
export class MatchRelevancy {
|
||||
constructor(public match: any,
|
||||
public level: number,
|
||||
public relevancy: number) {
|
||||
|
@@ -1,70 +1,16 @@
|
||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||
import { Context } from '../../../../core/shared/context.model';
|
||||
import { hasNoValue, hasValue, isNotEmpty } from '../../../empty.util';
|
||||
import { hasNoValue, hasValue } from '../../../empty.util';
|
||||
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||
import { ListableObject } from '../listable-object.model';
|
||||
import { environment } from '../../../../../environments/environment';
|
||||
import { ThemeConfig } from '../../../../../config/theme.config';
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import {
|
||||
DEFAULT_CONTEXT,
|
||||
DEFAULT_THEME,
|
||||
DEFAULT_VIEW_MODE,
|
||||
MatchRelevancy, resolveTheme
|
||||
} from '../listable-object/listable-object.decorator';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
|
||||
export const DEFAULT_VIEW_MODE = ViewMode.ListElement;
|
||||
export const DEFAULT_CONTEXT = Context.Any;
|
||||
export const DEFAULT_THEME = '*';
|
||||
|
||||
/**
|
||||
* A class used to compare two matches and their relevancy to determine which of the two gains priority over the other
|
||||
*
|
||||
* "level" represents the index of the first default value that was used to find the match with:
|
||||
* ViewMode being index 0, Context index 1 and theme index 2. Examples:
|
||||
* - If a default value was used for context, but not view-mode and theme, the "level" will be 1
|
||||
* - If a default value was used for view-mode and context, but not for theme, the "level" will be 0
|
||||
* - If no default value was used for any of the fields, the "level" will be 3
|
||||
*
|
||||
* "relevancy" represents the amount of values that didn't require a default value to fall back on. Examples:
|
||||
* - If a default value was used for theme, but not view-mode and context, the "relevancy" will be 2
|
||||
* - If a default value was used for view-mode and context, but not for theme, the "relevancy" will be 1
|
||||
* - If a default value was used for all fields, the "relevancy" will be 0
|
||||
* - If no default value was used for any of the fields, the "relevancy" will be 3
|
||||
*
|
||||
* To determine which of two MatchRelevancies is the most relevant, we compare "level" and "relevancy" in that order.
|
||||
* If any of the two is higher than the other, that match is most relevant. Examples:
|
||||
* - { level: 1, relevancy: 1 } is more relevant than { level: 0, relevancy: 2 }
|
||||
* - { level: 1, relevancy: 1 } is less relevant than { level: 1, relevancy: 2 }
|
||||
* - { level: 1, relevancy: 1 } is more relevant than { level: 1, relevancy: 0 }
|
||||
* - { level: 1, relevancy: 1 } is less relevant than { level: 2, relevancy: 0 }
|
||||
* - { level: 1, relevancy: 1 } is more relevant than null
|
||||
*/
|
||||
class MatchRelevancy {
|
||||
constructor(public match: any,
|
||||
public level: number,
|
||||
public relevancy: number) {
|
||||
}
|
||||
|
||||
isMoreRelevantThan(otherMatch: MatchRelevancy): boolean {
|
||||
if (hasNoValue(otherMatch)) {
|
||||
return true;
|
||||
}
|
||||
if (otherMatch.level > this.level) {
|
||||
return false;
|
||||
}
|
||||
if (otherMatch.level === this.level && otherMatch.relevancy > this.relevancy) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
isLessRelevantThan(otherMatch: MatchRelevancy): boolean {
|
||||
return !this.isMoreRelevantThan(otherMatch);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory to allow us to inject getThemeConfigFor so we can mock it in tests
|
||||
*/
|
||||
export const GET_THEME_CONFIG_FOR_FACTORY = new InjectionToken<(str) => ThemeConfig>('getThemeConfigFor', {
|
||||
providedIn: 'root',
|
||||
factory: () => getThemeConfigFor
|
||||
});
|
||||
|
||||
const map = new Map();
|
||||
|
||||
@@ -75,7 +21,7 @@ const map = new Map();
|
||||
* @param context The optional context the component represents
|
||||
* @param theme The optional theme for the component
|
||||
*/
|
||||
export function tabulatableObjectsComponent(objectsType: string | GenericConstructor<ListableObject>, viewMode: ViewMode, context: Context = DEFAULT_CONTEXT, theme = DEFAULT_THEME) {
|
||||
export function tabulatableObjectsComponent(objectsType: string | GenericConstructor<PaginatedList<ListableObject>>, viewMode: ViewMode, context: Context = DEFAULT_CONTEXT, theme = DEFAULT_THEME) {
|
||||
return function decorator(component: any) {
|
||||
if (hasNoValue(objectsType)) {
|
||||
return;
|
||||
@@ -107,7 +53,7 @@ export function tabulatableObjectsComponent(objectsType: string | GenericConstru
|
||||
export function getTabulatableObjectsComponent(types: (string | GenericConstructor<ListableObject>)[], viewMode: ViewMode, context: Context = DEFAULT_CONTEXT, theme: string = DEFAULT_THEME) {
|
||||
let currentBestMatch: MatchRelevancy = null;
|
||||
for (const type of types) {
|
||||
const typeMap = map.get(type);
|
||||
const typeMap = map.get(PaginatedList<typeof type>);
|
||||
if (hasValue(typeMap)) {
|
||||
const match = getMatch(typeMap, [viewMode, context, theme], [DEFAULT_VIEW_MODE, DEFAULT_CONTEXT, DEFAULT_THEME]);
|
||||
if (hasNoValue(currentBestMatch) || currentBestMatch.isLessRelevantThan(match)) {
|
||||
@@ -160,35 +106,3 @@ function getMatch(typeMap: Map<any, any>, keys: any[], defaults: any[]): MatchRe
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for a ThemeConfig by its name;
|
||||
*/
|
||||
export const getThemeConfigFor = (themeName: string): ThemeConfig => {
|
||||
return environment.themes.find(theme => theme.name === themeName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find a match in the given map for the given theme name, taking theme extension into account
|
||||
*
|
||||
* @param contextMap A map of theme names to components
|
||||
* @param themeName The name of the theme to check
|
||||
* @param checkedThemeNames The list of theme names that are already checked
|
||||
*/
|
||||
export const resolveTheme = (contextMap: Map<any, any>, themeName: string, checkedThemeNames: string[] = []): any => {
|
||||
const match = contextMap.get(themeName);
|
||||
if (hasValue(match)) {
|
||||
return match;
|
||||
} else {
|
||||
const cfg = getThemeConfigFor(themeName);
|
||||
if (hasValue(cfg) && isNotEmpty(cfg.extends)) {
|
||||
const nextTheme = cfg.extends;
|
||||
const nextCheckedThemeNames = [...checkedThemeNames, themeName];
|
||||
if (checkedThemeNames.includes(nextTheme)) {
|
||||
throw new Error('Theme extension cycle detected: ' + [...nextCheckedThemeNames, nextTheme].join(' -> '));
|
||||
} else {
|
||||
return resolveTheme(contextMap, nextTheme, nextCheckedThemeNames);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -2,17 +2,17 @@ import { Component, Inject } from '@angular/core';
|
||||
import {
|
||||
AbstractTabulatableElementComponent
|
||||
} from '../../../object-collection/shared/objects-collection-tabulatable/objects-collection-tabulatable.component';
|
||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||
import { TruncatableService } from '../../../truncatable/truncatable.service';
|
||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||
import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { SearchResult } from '../../../search/models/search-result.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-result-list-element',
|
||||
template: ``
|
||||
})
|
||||
export class TabulatableResultListElementsComponent<T extends PaginatedList<K>, K extends DSpaceObject> extends AbstractTabulatableElementComponent<T> {
|
||||
export class TabulatableResultListElementsComponent<T extends PaginatedList<K>, K extends SearchResult<any>> extends AbstractTabulatableElementComponent<T> {
|
||||
public constructor(protected truncatableService: TruncatableService,
|
||||
public dsoNameService: DSONameService,
|
||||
@Inject(APP_CONFIG) protected appConfig?: AppConfig) {
|
||||
|
@@ -15,9 +15,10 @@
|
||||
(paginationChange)="onPaginationChange($event)"
|
||||
(prev)="goPrev()"
|
||||
(next)="goNext()"
|
||||
[retainScrollPosition]="true"
|
||||
>
|
||||
<div class="row" *ngIf="objects?.hasSucceeded">
|
||||
<div @fadeIn>
|
||||
<div @fadeIn>
|
||||
<ds-tabulatable-objects-loader [objects]="objects.payload"
|
||||
[context]="context"
|
||||
[showThumbnails]="showThumbnails"
|
||||
|
@@ -147,13 +147,6 @@ export class ObjectTableComponent {
|
||||
this._objects$ = new BehaviorSubject(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the instance variables
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
console.log('table rendered');
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the current page when it changes
|
||||
* @param event The new page
|
||||
@@ -205,5 +198,4 @@ export class ObjectTableComponent {
|
||||
goNext() {
|
||||
this.next.emit(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user