mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge remote-tracking branch 'origin/main' into CST-5668
This commit is contained in:
@@ -25,7 +25,7 @@ services:
|
|||||||
### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' ####
|
### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' ####
|
||||||
# Ensure that the database is ready BEFORE starting tomcat
|
# Ensure that the database is ready BEFORE starting tomcat
|
||||||
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
||||||
# 2. Then, run database migration to init database tables
|
# 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any)
|
||||||
# 3. (Custom for Entities) enable Entity-specific collection submission mappings in item-submission.xml
|
# 3. (Custom for Entities) enable Entity-specific collection submission mappings in item-submission.xml
|
||||||
# This 'sed' command inserts the sample configurations specific to the Entities data set, see:
|
# This 'sed' command inserts the sample configurations specific to the Entities data set, see:
|
||||||
# https://github.com/DSpace/DSpace/blob/main/dspace/config/item-submission.xml#L36-L49
|
# https://github.com/DSpace/DSpace/blob/main/dspace/config/item-submission.xml#L36-L49
|
||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
- '-c'
|
- '-c'
|
||||||
- |
|
- |
|
||||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||||
/dspace/bin/dspace database migrate
|
/dspace/bin/dspace database migrate ignored
|
||||||
sed -i '/name-map collection-handle="default".*/a \\n <name-map collection-handle="123456789/3" submission-name="Publication"/> \
|
sed -i '/name-map collection-handle="default".*/a \\n <name-map collection-handle="123456789/3" submission-name="Publication"/> \
|
||||||
<name-map collection-handle="123456789/4" submission-name="Publication"/> \
|
<name-map collection-handle="123456789/4" submission-name="Publication"/> \
|
||||||
<name-map collection-handle="123456789/281" submission-name="Publication"/> \
|
<name-map collection-handle="123456789/281" submission-name="Publication"/> \
|
||||||
|
@@ -46,14 +46,14 @@ services:
|
|||||||
- solr_configs:/dspace/solr
|
- solr_configs:/dspace/solr
|
||||||
# Ensure that the database is ready BEFORE starting tomcat
|
# Ensure that the database is ready BEFORE starting tomcat
|
||||||
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
||||||
# 2. Then, run database migration to init database tables
|
# 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any)
|
||||||
# 3. Finally, start Tomcat
|
# 3. Finally, start Tomcat
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/bash
|
- /bin/bash
|
||||||
- '-c'
|
- '-c'
|
||||||
- |
|
- |
|
||||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||||
/dspace/bin/dspace database migrate
|
/dspace/bin/dspace database migrate ignored
|
||||||
catalina.sh run
|
catalina.sh run
|
||||||
# DSpace database container
|
# DSpace database container
|
||||||
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
||||||
|
@@ -18,8 +18,8 @@ import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-r
|
|||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||||
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
||||||
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
import { AccessStatusDataService } from 'src/app/core/data/access-status-data.service';
|
import { AccessStatusDataService } from '../../../../../core/data/access-status-data.service';
|
||||||
import { AccessStatusObject } from 'src/app/shared/object-list/access-status-badge/access-status.model';
|
import { AccessStatusObject } from '../../../../../shared/object-list/access-status-badge/access-status.model';
|
||||||
|
|
||||||
describe('ItemAdminSearchResultGridElementComponent', () => {
|
describe('ItemAdminSearchResultGridElementComponent', () => {
|
||||||
let component: ItemAdminSearchResultGridElementComponent;
|
let component: ItemAdminSearchResultGridElementComponent;
|
||||||
|
@@ -7,13 +7,11 @@
|
|||||||
class="lead"
|
class="lead"
|
||||||
[innerHTML]="firstMetadataValue('organization.legalName')"></span>
|
[innerHTML]="firstMetadataValue('organization.legalName')"></span>
|
||||||
<span class="text-muted">
|
<span class="text-muted">
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="3">
|
|
||||||
<span *ngIf="dso.allMetadata(['dc.description']).length > 0"
|
<span *ngIf="dso.allMetadata(['dc.description']).length > 0"
|
||||||
class="item-list-org-unit-description">
|
class="item-list-org-unit-description">
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="3"><span
|
<ds-truncatable-part [id]="dso.id" [minLines]="3"><span
|
||||||
[innerHTML]="firstMetadataValue('dc.description')"></span>
|
[innerHTML]="firstMetadataValue('dc.description')"></span>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
</span>
|
</span>
|
||||||
</ds-truncatable-part>
|
|
||||||
</span>
|
</span>
|
||||||
</ds-truncatable>
|
</ds-truncatable>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbTooltipModule, NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import { EditItemPageRoutingModule } from './edit-item-page.routing.module';
|
import { EditItemPageRoutingModule } from './edit-item-page.routing.module';
|
||||||
@@ -48,7 +48,8 @@ import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-
|
|||||||
EditItemPageRoutingModule,
|
EditItemPageRoutingModule,
|
||||||
SearchPageModule,
|
SearchPageModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
ResourcePoliciesModule
|
ResourcePoliciesModule,
|
||||||
|
NgbModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
EditItemPageComponent,
|
EditItemPageComponent,
|
||||||
|
@@ -1,13 +1,33 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<ds-alert [type]="'alert-info'" [content]="'item.edit.authorizations.heading'"></ds-alert>
|
<ds-alert [type]="'alert-info'" [content]="'item.edit.authorizations.heading'"></ds-alert>
|
||||||
<ds-resource-policies [resourceType]="'item'" [resourceUUID]="(getItemUUID() | async)"></ds-resource-policies>
|
<ds-resource-policies [resourceType]="'item'" [resourceName]="(getItemName() | async)"
|
||||||
<ng-container *ngFor="let bundle of (getItemBundles() | async); trackById">
|
[resourceUUID]="(getItemUUID() | async)">
|
||||||
<ds-resource-policies [resourceType]="'bundle'"
|
</ds-resource-policies>
|
||||||
[resourceUUID]="bundle.id"></ds-resource-policies>
|
<ng-container *ngFor="let bundle of (bundles$ | async); trackById">
|
||||||
<ng-container *ngFor="let bitstream of (bundleBitstreamsMap.get(bundle.id) | async)?.page; trackById">
|
<ds-resource-policies [resourceType]="'bundle'" [resourceUUID]="bundle.id" [resourceName]="bundle.name">
|
||||||
<ds-resource-policies [resourceType]="'bitstream'"
|
</ds-resource-policies>
|
||||||
[resourceUUID]="bitstream.id"></ds-resource-policies>
|
<ng-container *ngIf="(bundleBitstreamsMap.get(bundle.id)?.bitstreams | async)?.length > 0">
|
||||||
</ng-container>
|
<div class="card auth-bitstream-container">
|
||||||
</ng-container>
|
<div class="card-header">
|
||||||
|
<button type="button" class="btn btn-outline-primary" (click)="collapseArea(bundle.id)"
|
||||||
|
[attr.aria-expanded]="false" [attr.aria-controls]="bundle.id">
|
||||||
|
{{ 'collection.edit.item.authorizations.show-bitstreams-button' | translate }} {{bundle.name}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" [id]="bundle.id" [ngbCollapse]="bundleBitstreamsMap.get(bundle.id).isCollapsed">
|
||||||
|
<ng-container
|
||||||
|
*ngFor="let bitstream of (bundleBitstreamsMap.get(bundle.id).bitstreams | async); trackById">
|
||||||
|
<ds-resource-policies [resourceType]="'bitstream'" [resourceUUID]="bitstream.id"
|
||||||
|
[resourceName]="bitstream.name"></ds-resource-policies>
|
||||||
|
</ng-container>
|
||||||
|
<div class="row justify-content-center" *ngIf="!bundleBitstreamsMap.get(bundle.id).allBitstreamsLoaded">
|
||||||
|
<button type="button" class="btn btn-link" (click)="onBitstreamsLoad(bundle)">{{ 'collection.edit.item.authorizations.load-more-button' | translate }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
<div class="row justify-content-center" *ngIf="!allBundlesLoaded">
|
||||||
|
<button type="button" class="btn btn-link" (click)="onBundleLoad()">{{ 'collection.edit.item.authorizations.load-bundle-button' | translate }}</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
.auth-bitstream-container {
|
||||||
|
margin-top: -1em;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
@@ -1,11 +1,12 @@
|
|||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { waitForAsync, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
import { waitForAsync, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||||
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf, of } from 'rxjs';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { cold } from 'jasmine-marbles';
|
import { cold } from 'jasmine-marbles';
|
||||||
import { ItemAuthorizationsComponent } from './item-authorizations.component';
|
import { ItemAuthorizationsComponent, BitstreamMapValue } from './item-authorizations.component';
|
||||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||||
import { Bundle } from '../../../core/shared/bundle.model';
|
import { Bundle } from '../../../core/shared/bundle.model';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
@@ -57,8 +58,6 @@ describe('ItemAuthorizationsComponent test suite', () => {
|
|||||||
bitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream3, bitstream4]))
|
bitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream3, bitstream4]))
|
||||||
});
|
});
|
||||||
const bundles = [bundle1, bundle2];
|
const bundles = [bundle1, bundle2];
|
||||||
const bitstreamList1: PaginatedList<Bitstream> = buildPaginatedList(new PageInfo(), [bitstream1, bitstream2]);
|
|
||||||
const bitstreamList2: PaginatedList<Bitstream> = buildPaginatedList(new PageInfo(), [bitstream3, bitstream4]);
|
|
||||||
|
|
||||||
const item = Object.assign(new Item(), {
|
const item = Object.assign(new Item(), {
|
||||||
uuid: 'item',
|
uuid: 'item',
|
||||||
@@ -142,13 +141,12 @@ describe('ItemAuthorizationsComponent test suite', () => {
|
|||||||
expect(compAsAny.bundleBitstreamsMap.has('bundle1')).toBeTruthy();
|
expect(compAsAny.bundleBitstreamsMap.has('bundle1')).toBeTruthy();
|
||||||
expect(compAsAny.bundleBitstreamsMap.has('bundle2')).toBeTruthy();
|
expect(compAsAny.bundleBitstreamsMap.has('bundle2')).toBeTruthy();
|
||||||
let bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle1');
|
let bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle1');
|
||||||
expect(bitstreamList).toBeObservable(cold('(a|)', {
|
expect(bitstreamList.bitstreams).toBeObservable(cold('(a|)', {
|
||||||
a: bitstreamList1
|
a : [bitstream1, bitstream2]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle2');
|
bitstreamList = compAsAny.bundleBitstreamsMap.get('bundle2');
|
||||||
expect(bitstreamList).toBeObservable(cold('(a|)', {
|
expect(bitstreamList.bitstreams).toBeObservable(cold('(a|)', {
|
||||||
a: bitstreamList2
|
a: [bitstream3, bitstream4]
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { isEqual } from 'lodash';
|
||||||
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
@@ -6,7 +8,8 @@ import { catchError, filter, first, map, mergeMap, take } from 'rxjs/operators';
|
|||||||
|
|
||||||
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import {
|
import {
|
||||||
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataWithNotEmptyPayload,
|
getFirstSucceededRemoteDataPayload,
|
||||||
|
getFirstSucceededRemoteDataWithNotEmptyPayload,
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
@@ -25,7 +28,8 @@ interface BundleBitstreamsMapEntry {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-authorizations',
|
selector: 'ds-item-authorizations',
|
||||||
templateUrl: './item-authorizations.component.html'
|
templateUrl: './item-authorizations.component.html',
|
||||||
|
styleUrls:['./item-authorizations.component.scss']
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* Component that handles the item Authorizations
|
* Component that handles the item Authorizations
|
||||||
@@ -36,13 +40,13 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
|||||||
* A map that contains all bitstream of the item's bundles
|
* A map that contains all bitstream of the item's bundles
|
||||||
* @type {Observable<Map<string, Observable<PaginatedList<Bitstream>>>>}
|
* @type {Observable<Map<string, Observable<PaginatedList<Bitstream>>>>}
|
||||||
*/
|
*/
|
||||||
public bundleBitstreamsMap: Map<string, Observable<PaginatedList<Bitstream>>> = new Map<string, Observable<PaginatedList<Bitstream>>>();
|
public bundleBitstreamsMap: Map<string, BitstreamMapValue> = new Map<string, BitstreamMapValue>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of bundle for the item
|
* The list of all bundles for the item
|
||||||
* @type {Observable<PaginatedList<Bundle>>}
|
* @type {Observable<PaginatedList<Bundle>>}
|
||||||
*/
|
*/
|
||||||
private bundles$: BehaviorSubject<Bundle[]> = new BehaviorSubject<Bundle[]>([]);
|
bundles$: BehaviorSubject<Bundle[]> = new BehaviorSubject<Bundle[]>([]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The target editing item
|
* The target editing item
|
||||||
@@ -56,15 +60,48 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
private subs: Subscription[] = [];
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the bundles to be loaded on demand
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
bundlesPerPage = 6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of current page
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
bundlesPageSize = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The flag to show or not the 'Load more' button
|
||||||
|
* based on the condition if all the bundles are loaded or not
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
allBundlesLoaded = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial size of loaded bitstreams
|
||||||
|
* The size of incrementation used in bitstream pagination
|
||||||
|
*/
|
||||||
|
bitstreamSize = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The size of the loaded bitstremas at a certain moment
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private bitstreamPageSize = 4;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize instance variables
|
* Initialize instance variables
|
||||||
*
|
*
|
||||||
* @param {LinkService} linkService
|
* @param {LinkService} linkService
|
||||||
* @param {ActivatedRoute} route
|
* @param {ActivatedRoute} route
|
||||||
|
* @param nameService
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private linkService: LinkService,
|
private linkService: LinkService,
|
||||||
private route: ActivatedRoute
|
private route: ActivatedRoute,
|
||||||
|
private nameService: DSONameService
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,12 +109,49 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
|||||||
* Initialize the component, setting up the bundle and bitstream within the item
|
* Initialize the component, setting up the bundle and bitstream within the item
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.getBundlesPerItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the item's UUID
|
||||||
|
*/
|
||||||
|
getItemUUID(): Observable<string> {
|
||||||
|
return this.item$.pipe(
|
||||||
|
map((item: Item) => item.id),
|
||||||
|
first((UUID: string) => isNotEmpty(UUID))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the item's name
|
||||||
|
*/
|
||||||
|
getItemName(): Observable<string> {
|
||||||
|
return this.item$.pipe(
|
||||||
|
map((item: Item) => this.nameService.getName(item))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all item's bundles
|
||||||
|
*
|
||||||
|
* @return an observable that emits all item's bundles
|
||||||
|
*/
|
||||||
|
getItemBundles(): Observable<Bundle[]> {
|
||||||
|
return this.bundles$.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all bundles per item
|
||||||
|
* and all the bitstreams per bundle
|
||||||
|
* @param page number of current page
|
||||||
|
*/
|
||||||
|
getBundlesPerItem(page: number = 1) {
|
||||||
this.item$ = this.route.data.pipe(
|
this.item$ = this.route.data.pipe(
|
||||||
map((data) => data.dso),
|
map((data) => data.dso),
|
||||||
getFirstSucceededRemoteDataWithNotEmptyPayload(),
|
getFirstSucceededRemoteDataWithNotEmptyPayload(),
|
||||||
map((item: Item) => this.linkService.resolveLink(
|
map((item: Item) => this.linkService.resolveLink(
|
||||||
item,
|
item,
|
||||||
followLink('bundles', {}, followLink('bitstreams'))
|
followLink('bundles', {findListOptions: {currentPage : page, elementsPerPage: this.bundlesPerPage}}, followLink('bitstreams'))
|
||||||
))
|
))
|
||||||
) as Observable<Item>;
|
) as Observable<Item>;
|
||||||
|
|
||||||
@@ -96,37 +170,36 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
|||||||
take(1),
|
take(1),
|
||||||
map((list: PaginatedList<Bundle>) => list.page)
|
map((list: PaginatedList<Bundle>) => list.page)
|
||||||
).subscribe((bundles: Bundle[]) => {
|
).subscribe((bundles: Bundle[]) => {
|
||||||
|
if (isEqual(bundles.length,0) || bundles.length < this.bundlesPerPage) {
|
||||||
|
this.allBundlesLoaded = true;
|
||||||
|
}
|
||||||
|
if (isEqual(page, 1)) {
|
||||||
this.bundles$.next(bundles);
|
this.bundles$.next(bundles);
|
||||||
|
} else {
|
||||||
|
this.bundles$.next(this.bundles$.getValue().concat(bundles));
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
bundles$.pipe(
|
bundles$.pipe(
|
||||||
take(1),
|
take(1),
|
||||||
mergeMap((list: PaginatedList<Bundle>) => list.page),
|
mergeMap((list: PaginatedList<Bundle>) => list.page),
|
||||||
map((bundle: Bundle) => ({ id: bundle.id, bitstreams: this.getBundleBitstreams(bundle) }))
|
map((bundle: Bundle) => ({ id: bundle.id, bitstreams: this.getBundleBitstreams(bundle) }))
|
||||||
).subscribe((entry: BundleBitstreamsMapEntry) => {
|
).subscribe((entry: BundleBitstreamsMapEntry) => {
|
||||||
this.bundleBitstreamsMap.set(entry.id, entry.bitstreams);
|
let bitstreamMapValues: BitstreamMapValue = {
|
||||||
|
isCollapsed: true,
|
||||||
|
allBitstreamsLoaded: false,
|
||||||
|
bitstreams: null
|
||||||
|
};
|
||||||
|
bitstreamMapValues.bitstreams = entry.bitstreams.pipe(
|
||||||
|
map((b: PaginatedList<Bitstream>) => {
|
||||||
|
bitstreamMapValues.allBitstreamsLoaded = b?.page.length < this.bitstreamSize;
|
||||||
|
return [...b.page.slice(0, this.bitstreamSize)];
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
this.bundleBitstreamsMap.set(entry.id, bitstreamMapValues);
|
||||||
|
})
|
||||||
/**
|
|
||||||
* Return the item's UUID
|
|
||||||
*/
|
|
||||||
getItemUUID(): Observable<string> {
|
|
||||||
return this.item$.pipe(
|
|
||||||
map((item: Item) => item.id),
|
|
||||||
first((UUID: string) => isNotEmpty(UUID))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return all item's bundles
|
|
||||||
*
|
|
||||||
* @return an observable that emits all item's bundles
|
|
||||||
*/
|
|
||||||
getItemBundles(): Observable<Bundle[]> {
|
|
||||||
return this.bundles$.asObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return all bundle's bitstreams
|
* Return all bundle's bitstreams
|
||||||
*
|
*
|
||||||
@@ -142,6 +215,46 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the collapsible state of the area that contains the bitstream list
|
||||||
|
* @param bundleId Id of bundle responsible for the requested bitstreams
|
||||||
|
*/
|
||||||
|
collapseArea(bundleId: string) {
|
||||||
|
this.bundleBitstreamsMap.get(bundleId).isCollapsed = !this.bundleBitstreamsMap.get(bundleId).isCollapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads as much bundles as initial value of bundleSize to be displayed
|
||||||
|
*/
|
||||||
|
onBundleLoad(){
|
||||||
|
this.bundlesPageSize ++;
|
||||||
|
this.getBundlesPerItem(this.bundlesPageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the bitstreams that are going to be loaded on demand,
|
||||||
|
* based on the number configured on this.bitstreamSize.
|
||||||
|
* @param bundle parent of bitstreams that are requested to be shown
|
||||||
|
* @returns Subscription
|
||||||
|
*/
|
||||||
|
onBitstreamsLoad(bundle: Bundle) {
|
||||||
|
return this.getBundleBitstreams(bundle).subscribe((res: PaginatedList<Bitstream>) => {
|
||||||
|
let nextBitstreams = res?.page.slice(this.bitstreamPageSize, this.bitstreamPageSize + this.bitstreamSize);
|
||||||
|
let bitstreamsToShow = this.bundleBitstreamsMap.get(bundle.id).bitstreams.pipe(
|
||||||
|
map((existingBits: Bitstream[])=> {
|
||||||
|
return [... existingBits, ...nextBitstreams];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.bitstreamPageSize = this.bitstreamPageSize + this.bitstreamSize;
|
||||||
|
let bitstreamMapValues: BitstreamMapValue = {
|
||||||
|
bitstreams: bitstreamsToShow ,
|
||||||
|
isCollapsed: this.bundleBitstreamsMap.get(bundle.id).isCollapsed,
|
||||||
|
allBitstreamsLoaded: res?.page.length <= this.bitstreamPageSize
|
||||||
|
};
|
||||||
|
this.bundleBitstreamsMap.set(bundle.id, bitstreamMapValues);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unsubscribe from all subscriptions
|
* Unsubscribe from all subscriptions
|
||||||
*/
|
*/
|
||||||
@@ -151,3 +264,9 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
|||||||
.forEach((subscription) => subscription.unsubscribe());
|
.forEach((subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BitstreamMapValue {
|
||||||
|
bitstreams: Observable<Bitstream[]>;
|
||||||
|
isCollapsed: boolean;
|
||||||
|
allBitstreamsLoaded: boolean;
|
||||||
|
}
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
<div class="card" [@focusShadow]="(isCollapsed$ | async)?'blur':'focus'">
|
<div class="card" [@focusShadow]="(isCollapsed$ | async)?'blur':'focus'">
|
||||||
<ds-truncatable [id]="dso.id">
|
|
||||||
<div class="position-absolute ml-1">
|
<div class="position-absolute ml-1">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,28 +18,28 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||||
<ds-access-status-badge [item]="dso"></ds-access-status-badge>
|
<ds-access-status-badge [item]="dso"></ds-access-status-badge>
|
||||||
|
<ds-truncatable [id]="dso.id">
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
|
||||||
<h4 class="card-title" [innerHTML]="firstMetadataValue('dc.title')"></h4>
|
<h4 class="card-title" [innerHTML]="firstMetadataValue('dc.title')"></h4>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
<p *ngIf="dso.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])"
|
<ds-truncatable-part [id]="dso.id" [minLines]="1" *ngIf="dso.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])">
|
||||||
class="item-authors card-text text-muted">
|
<p class="item-authors card-text text-muted">
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
|
||||||
<span *ngIf="dso.hasMetadata('dc.date.issued')" class="item-date">{{firstMetadataValue('dc.date.issued')}}</span>
|
<span *ngIf="dso.hasMetadata('dc.date.issued')" class="item-date">{{firstMetadataValue('dc.date.issued')}}</span>
|
||||||
<span *ngFor="let author of allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);">,
|
<span *ngFor="let author of allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);">,
|
||||||
<span [innerHTML]="author"></span>
|
<span [innerHTML]="author"></span>
|
||||||
</span>
|
</span>
|
||||||
</ds-truncatable-part>
|
|
||||||
</p>
|
</p>
|
||||||
<p *ngIf="dso.hasMetadata('dc.description.abstract')" class="item-abstract card-text">
|
</ds-truncatable-part>
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="3">
|
<ds-truncatable-part *ngIf="dso.hasMetadata('dc.description.abstract')" [id]="dso.id" [minLines]="3">
|
||||||
|
<p class="item-abstract card-text">
|
||||||
<span [innerHTML]="firstMetadataValue('dc.description.abstract')"></span>
|
<span [innerHTML]="firstMetadataValue('dc.description.abstract')"></span>
|
||||||
</ds-truncatable-part>
|
|
||||||
</p>
|
</p>
|
||||||
|
</ds-truncatable-part>
|
||||||
|
</ds-truncatable>
|
||||||
<div *ngIf="linkType != linkTypes.None" class="text-center">
|
<div *ngIf="linkType != linkTypes.None" class="text-center">
|
||||||
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||||
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a>
|
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ds-truncatable>
|
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
<ds-truncatable-part [maxLines]="1" [background]="isCurrent() ? 'primary' : 'default'">
|
<ds-truncatable-part [maxLines]="1" [background]="isCurrent() ? 'primary' : 'default'" [showToggle]="false">
|
||||||
<div [ngClass]="isCurrent() ? 'text-light' : 'text-body'"
|
<div [ngClass]="isCurrent() ? 'text-light' : 'text-body'"
|
||||||
[innerHTML]="(parentTitle$ && parentTitle$ | async) ? (parentTitle$ | async) : ('home.breadcrumbs' | translate)"></div>
|
[innerHTML]="(parentTitle$ && parentTitle$ | async) ? (parentTitle$ | async) : ('home.breadcrumbs' | translate)"></div>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
<ds-truncatable-part *ngIf="title" [maxLines]="1" [background]="isCurrent() ? 'primary' : 'default'">
|
<ds-truncatable-part *ngIf="title" [maxLines]="1" [background]="isCurrent() ? 'primary' : 'default'" [showToggle]="false">
|
||||||
<div class="font-weight-bold"
|
<div class="font-weight-bold"
|
||||||
[ngClass]="isCurrent() ? 'text-light' : 'text-primary'"
|
[ngClass]="isCurrent() ? 'text-light' : 'text-primary'"
|
||||||
[innerHTML]="title"></div>
|
[innerHTML]="title"></div>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
<ds-truncatable-part *ngIf="description" [maxLines]="1" [background]="isCurrent() ? 'primary' : 'default'">
|
<ds-truncatable-part *ngIf="description" [maxLines]="1" [background]="isCurrent() ? 'primary' : 'default'" [showToggle]="false">
|
||||||
<div class="text-secondary"
|
<div class="text-secondary"
|
||||||
[ngClass]="isCurrent() ? 'text-light' : 'text-secondary'"
|
[ngClass]="isCurrent() ? 'text-light' : 'text-secondary'"
|
||||||
[innerHTML]="description"></div>
|
[innerHTML]="description"></div>
|
||||||
|
@@ -4,9 +4,15 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th colspan="10">
|
<th colspan="10">
|
||||||
<div class="d-flex justify-content-between align-items-center m-0">
|
<div class="d-flex justify-content-between align-items-center m-0">
|
||||||
{{ 'resource-policies.table.headers.title.for.' + resourceType | translate }} {{resourceUUID}}
|
<span>
|
||||||
|
{{ 'resource-policies.table.headers.title.for.' + resourceType | translate }}
|
||||||
|
<span class="text-info"> {{resourceName}} </span>
|
||||||
|
<ng-container *ngIf="resourceType != 'item'">
|
||||||
|
({{resourceUUID}})
|
||||||
|
</ng-container>
|
||||||
|
</span>
|
||||||
<div class="space-children-mr">
|
<div class="space-children-mr">
|
||||||
<button class="btn btn-danger"
|
<button class="btn btn-danger p-1"
|
||||||
[disabled]="(!(canDelete() | async)) || (isProcessingDelete() | async)"
|
[disabled]="(!(canDelete() | async)) || (isProcessingDelete() | async)"
|
||||||
[title]="'resource-policies.delete.btn.title' | translate"
|
[title]="'resource-policies.delete.btn.title' | translate"
|
||||||
(click)="deleteSelectedResourcePolicies()">
|
(click)="deleteSelectedResourcePolicies()">
|
||||||
@@ -18,7 +24,7 @@
|
|||||||
{{'resource-policies.delete.btn' | translate}}
|
{{'resource-policies.delete.btn' | translate}}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-success"
|
<button class="btn btn-success p-1"
|
||||||
[disabled]="(isProcessingDelete() | async)"
|
[disabled]="(isProcessingDelete() | async)"
|
||||||
[title]="'resource-policies.add.for.' + resourceType | translate"
|
[title]="'resource-policies.add.for.' + resourceType | translate"
|
||||||
(click)="redirectToResourcePolicyCreatePage()">
|
(click)="redirectToResourcePolicyCreatePage()">
|
||||||
|
@@ -62,6 +62,12 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
@Input() public resourceType: string;
|
@Input() public resourceType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource name
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
@Input() public resourceName: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if component is active
|
* A boolean representing if component is active
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
@@ -1,5 +1,9 @@
|
|||||||
<div class="clamp-{{background}}-{{lines}} min-{{minLines}} {{type}} {{fixedHeight ? 'fixedHeight' : ''}}">
|
<div class="clamp-{{background}}-{{lines}} min-{{minLines}} {{type}} {{fixedHeight ? 'fixedHeight' : ''}}">
|
||||||
<div class="content dont-break-out">
|
<div #content class="content dont-break-out">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn btn-link p-0 expandButton" dsDragClick (actualClick)="toggle()">
|
||||||
|
<i class="fas fa-angle-down"></i> {{ 'item.truncatable-part.show-more' | translate }}</button>
|
||||||
|
<button class="btn btn-link p-0 collapseButton" dsDragClick (actualClick)="toggle()" *ngIf="expand && expandable">
|
||||||
|
<i class="fas fa-angle-up"></i> {{ 'item.truncatable-part.show-less' | translate }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
.content:not(.truncated) ~ button.expandButton {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.removeFaded.content::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@@ -4,10 +4,17 @@ import { TruncatablePartComponent } from './truncatable-part.component';
|
|||||||
import { TruncatableService } from '../truncatable.service';
|
import { TruncatableService } from '../truncatable.service';
|
||||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
|
import { getMockTranslateService } from '../../mocks/translate.service.mock';
|
||||||
|
import { TranslateLoaderMock } from '../../mocks/translate-loader.mock';
|
||||||
|
import { mockTruncatableService } from '../../mocks/mock-trucatable.service';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { NativeWindowRef, NativeWindowService } from '../../../core/services/window.service';
|
||||||
|
|
||||||
describe('TruncatablePartComponent', () => {
|
describe('TruncatablePartComponent', () => {
|
||||||
let comp: TruncatablePartComponent;
|
let comp: TruncatablePartComponent;
|
||||||
let fixture: ComponentFixture<TruncatablePartComponent>;
|
let fixture: ComponentFixture<TruncatablePartComponent>;
|
||||||
|
let translateService: TranslateService;
|
||||||
const id1 = '123';
|
const id1 = '123';
|
||||||
const id2 = '456';
|
const id2 = '456';
|
||||||
|
|
||||||
@@ -22,10 +29,19 @@ describe('TruncatablePartComponent', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
translateService = getMockTranslateService();
|
||||||
imports: [NoopAnimationsModule],
|
void TestBed.configureTestingModule({
|
||||||
|
imports: [NoopAnimationsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
declarations: [TruncatablePartComponent],
|
declarations: [TruncatablePartComponent],
|
||||||
providers: [
|
providers: [
|
||||||
|
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
||||||
{ provide: TruncatableService, useValue: truncatableServiceStub },
|
{ provide: TruncatableService, useValue: truncatableServiceStub },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
@@ -52,6 +68,11 @@ describe('TruncatablePartComponent', () => {
|
|||||||
it('lines should equal minlines', () => {
|
it('lines should equal minlines', () => {
|
||||||
expect((comp as any).lines).toEqual(comp.minLines.toString());
|
expect((comp as any).lines).toEqual(comp.minLines.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('collapseButton should be hidden', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('.collapseButton'));
|
||||||
|
expect(a).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When the item is expanded', () => {
|
describe('When the item is expanded', () => {
|
||||||
@@ -72,5 +93,63 @@ describe('TruncatablePartComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect((comp as any).lines).toEqual('none');
|
expect((comp as any).lines).toEqual('none');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('collapseButton should be shown', () => {
|
||||||
|
(comp as any).setLines();
|
||||||
|
(comp as any).expandable = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const a = fixture.debugElement.query(By.css('.collapseButton'));
|
||||||
|
expect(a).not.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TruncatablePartComponent', () => {
|
||||||
|
let comp: TruncatablePartComponent;
|
||||||
|
let fixture: ComponentFixture<TruncatablePartComponent>;
|
||||||
|
let translateService: TranslateService;
|
||||||
|
const identifier = '1234567890';
|
||||||
|
let truncatableService;
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
translateService = getMockTranslateService();
|
||||||
|
void TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
NoopAnimationsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
declarations: [TruncatablePartComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
||||||
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(TruncatablePartComponent, {
|
||||||
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(TruncatablePartComponent);
|
||||||
|
comp = fixture.componentInstance; // TruncatablePartComponent test instance
|
||||||
|
comp.id = identifier;
|
||||||
|
fixture.detectChanges();
|
||||||
|
truncatableService = (comp as any).service;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When toggle is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(truncatableService, 'toggle');
|
||||||
|
comp.toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call toggle on the TruncatableService', () => {
|
||||||
|
expect(truncatableService.toggle).toHaveBeenCalledWith(identifier);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
import { AfterViewChecked, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
import { TruncatableService } from '../truncatable.service';
|
import { TruncatableService } from '../truncatable.service';
|
||||||
import { hasValue } from '../../empty.util';
|
import { hasValue } from '../../empty.util';
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ import { hasValue } from '../../empty.util';
|
|||||||
* Component that truncates/clamps a piece of text
|
* Component that truncates/clamps a piece of text
|
||||||
* It needs a TruncatableComponent parent to identify it's current state
|
* It needs a TruncatableComponent parent to identify it's current state
|
||||||
*/
|
*/
|
||||||
export class TruncatablePartComponent implements OnInit, OnDestroy {
|
export class TruncatablePartComponent implements AfterViewChecked, OnInit, OnDestroy {
|
||||||
/**
|
/**
|
||||||
* Number of lines shown when the part is collapsed
|
* Number of lines shown when the part is collapsed
|
||||||
*/
|
*/
|
||||||
@@ -40,6 +40,17 @@ export class TruncatablePartComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
@Input() background = 'default';
|
@Input() background = 'default';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if to show or not the show/collapse toggle.
|
||||||
|
* This value must have the same value as the parent TruncatableComponent
|
||||||
|
*/
|
||||||
|
@Input() showToggle = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The view on the truncatable part
|
||||||
|
*/
|
||||||
|
@ViewChild('content', {static: true}) content: ElementRef;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current amount of lines shown of this part
|
* Current amount of lines shown of this part
|
||||||
*/
|
*/
|
||||||
@@ -49,9 +60,16 @@ export class TruncatablePartComponent implements OnInit, OnDestroy {
|
|||||||
* Subscription to unsubscribe from
|
* Subscription to unsubscribe from
|
||||||
*/
|
*/
|
||||||
private sub;
|
private sub;
|
||||||
|
/**
|
||||||
|
* store variable used for local to expand collapse
|
||||||
|
*/
|
||||||
|
expand = false;
|
||||||
|
/**
|
||||||
|
* variable to check if expandable
|
||||||
|
*/
|
||||||
|
expandable = false;
|
||||||
|
|
||||||
public constructor(private service: TruncatableService) {
|
public constructor(private service: TruncatableService) {}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize lines variable
|
* Initialize lines variable
|
||||||
@@ -67,12 +85,57 @@ export class TruncatablePartComponent implements OnInit, OnDestroy {
|
|||||||
this.sub = this.service.isCollapsed(this.id).subscribe((collapsed: boolean) => {
|
this.sub = this.service.isCollapsed(this.id).subscribe((collapsed: boolean) => {
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
this.lines = this.minLines.toString();
|
this.lines = this.minLines.toString();
|
||||||
|
this.expand = false;
|
||||||
} else {
|
} else {
|
||||||
this.lines = this.maxLines < 0 ? 'none' : this.maxLines.toString();
|
this.lines = this.maxLines < 0 ? 'none' : this.maxLines.toString();
|
||||||
|
this.expand = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngAfterViewChecked() {
|
||||||
|
this.truncateElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expands the truncatable when it's collapsed, collapses it when it's expanded
|
||||||
|
*/
|
||||||
|
public toggle() {
|
||||||
|
this.service.toggle(this.id);
|
||||||
|
this.expandable = !this.expandable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check for the truncate element
|
||||||
|
*/
|
||||||
|
public truncateElement() {
|
||||||
|
if (this.showToggle) {
|
||||||
|
const entry = this.content.nativeElement;
|
||||||
|
if (entry.scrollHeight > entry.offsetHeight) {
|
||||||
|
if (entry.children.length > 0) {
|
||||||
|
if (entry.children[entry.children.length - 1].offsetHeight > entry.offsetHeight) {
|
||||||
|
entry.classList.add('truncated');
|
||||||
|
entry.classList.remove('removeFaded');
|
||||||
|
} else {
|
||||||
|
entry.classList.remove('truncated');
|
||||||
|
entry.classList.add('removeFaded');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (entry.innerText.length > 0) {
|
||||||
|
entry.classList.add('truncated');
|
||||||
|
entry.classList.remove('removeFaded');
|
||||||
|
} else {
|
||||||
|
entry.classList.remove('truncated');
|
||||||
|
entry.classList.add('removeFaded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entry.classList.remove('truncated');
|
||||||
|
entry.classList.add('removeFaded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unsubscribe from the subscription
|
* Unsubscribe from the subscription
|
||||||
*/
|
*/
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
<div dsDragClick (actualClick)="toggle()" (mouseenter)="hoverExpand()" (mouseleave)="hoverCollapse()">
|
<div (mouseenter)="hoverExpand()" (mouseleave)="hoverCollapse()">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -70,15 +70,4 @@ describe('TruncatableComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When toggle is called', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(truncatableService, 'toggle');
|
|
||||||
comp.toggle();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call toggle on the TruncatableService', () => {
|
|
||||||
expect(truncatableService.toggle).toHaveBeenCalledWith(identifier);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
import {
|
import { AfterViewChecked, Component, ElementRef, Input, OnInit } from '@angular/core';
|
||||||
Component, Input
|
|
||||||
} from '@angular/core';
|
|
||||||
import { TruncatableService } from './truncatable.service';
|
import { TruncatableService } from './truncatable.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -13,7 +11,7 @@ import { TruncatableService } from './truncatable.service';
|
|||||||
/**
|
/**
|
||||||
* Component that represents a section with one or more truncatable parts that all listen to this state
|
* Component that represents a section with one or more truncatable parts that all listen to this state
|
||||||
*/
|
*/
|
||||||
export class TruncatableComponent {
|
export class TruncatableComponent implements OnInit, AfterViewChecked {
|
||||||
/**
|
/**
|
||||||
* Is true when all truncatable parts in this truncatable should be expanded on loading
|
* Is true when all truncatable parts in this truncatable should be expanded on loading
|
||||||
*/
|
*/
|
||||||
@@ -29,7 +27,13 @@ export class TruncatableComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() onHover = false;
|
@Input() onHover = false;
|
||||||
|
|
||||||
public constructor(private service: TruncatableService) {
|
/**
|
||||||
|
* A boolean representing if to show or not the show/collapse toggle
|
||||||
|
* This value must have the same value as the children TruncatablePartComponent
|
||||||
|
*/
|
||||||
|
@Input() showToggle = true;
|
||||||
|
|
||||||
|
public constructor(private service: TruncatableService, private el: ElementRef,) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,11 +65,18 @@ export class TruncatableComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
ngAfterViewChecked() {
|
||||||
* Expands the truncatable when it's collapsed, collapses it when it's expanded
|
if (this.showToggle) {
|
||||||
*/
|
const truncatedElements = this.el.nativeElement.querySelectorAll('.truncated');
|
||||||
public toggle() {
|
if (truncatedElements?.length > 0) {
|
||||||
this.service.toggle(this.id);
|
const truncateElements = this.el.nativeElement.querySelectorAll('.dont-break-out');
|
||||||
|
for (let i = 0; i < (truncateElements.length - 1); i++) {
|
||||||
|
truncateElements[i].classList.remove('truncated');
|
||||||
|
truncateElements[i].classList.add('notruncatable');
|
||||||
|
}
|
||||||
|
truncateElements[truncateElements.length - 1].classList.add('truncated');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -895,6 +895,8 @@
|
|||||||
"bitstream.edit.title": "Datei bearbeiten",
|
"bitstream.edit.title": "Datei bearbeiten",
|
||||||
|
|
||||||
|
|
||||||
|
// "browse.back.all-results": "All browse results",
|
||||||
|
"browse.back.all-results": "Filter zurücksetzen",
|
||||||
|
|
||||||
// "browse.comcol.by.author": "By Author",
|
// "browse.comcol.by.author": "By Author",
|
||||||
"browse.comcol.by.author": "Nach Autor:in",
|
"browse.comcol.by.author": "Nach Autor:in",
|
||||||
@@ -1782,6 +1784,18 @@
|
|||||||
// "dso-selector.placeholder": "Search for a {{ type }}",
|
// "dso-selector.placeholder": "Search for a {{ type }}",
|
||||||
"dso-selector.placeholder": "Suche nach {{ type }}",
|
"dso-selector.placeholder": "Suche nach {{ type }}",
|
||||||
|
|
||||||
|
// "dso-selector.select.collection.head": "Select a collection",
|
||||||
|
"dso-selector.select.collection.head": "Sammlung auswählen",
|
||||||
|
|
||||||
|
// "dso-selector.set-scope.community.head": "Select a search scope",
|
||||||
|
"dso-selector.set-scope.community.head": "Suchbereich auswählen",
|
||||||
|
|
||||||
|
// "dso-selector.set-scope.community.button": "Search all of DSpace",
|
||||||
|
"dso-selector.set-scope.community.button": "Alles durchsuchen",
|
||||||
|
|
||||||
|
// "dso-selector.set-scope.community.input-header": "Search for a community or collection",
|
||||||
|
"dso-selector.set-scope.community.input-header": "Suche Bereich oder Sammlung",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// "confirmation-modal.export-metadata.header": "Export metadata for {{ dsoName }}",
|
// "confirmation-modal.export-metadata.header": "Export metadata for {{ dsoName }}",
|
||||||
@@ -4293,6 +4307,9 @@
|
|||||||
// "search.filters.filter.author.placeholder": "Author name",
|
// "search.filters.filter.author.placeholder": "Author name",
|
||||||
"search.filters.filter.author.placeholder": "Autor:innenname",
|
"search.filters.filter.author.placeholder": "Autor:innenname",
|
||||||
|
|
||||||
|
// "search.filters.filter.author.label": "Search author name",
|
||||||
|
"search.filters.filter.author.label": "Autorensuche",
|
||||||
|
|
||||||
// "search.filters.filter.birthDate.head": "Birth Date",
|
// "search.filters.filter.birthDate.head": "Birth Date",
|
||||||
"search.filters.filter.birthDate.head": "Geburtsdatum",
|
"search.filters.filter.birthDate.head": "Geburtsdatum",
|
||||||
|
|
||||||
@@ -4472,6 +4489,8 @@
|
|||||||
// "search.form.search_mydspace": "Search MyDSpace",
|
// "search.form.search_mydspace": "Search MyDSpace",
|
||||||
"search.form.search_mydspace": "Suche im persönlichen Arbeitsbereich",
|
"search.form.search_mydspace": "Suche im persönlichen Arbeitsbereich",
|
||||||
|
|
||||||
|
// "search.form.scope.all": "All of DSpace",
|
||||||
|
"search.form.scope.all": "Alles durchsuchen",
|
||||||
|
|
||||||
|
|
||||||
// "search.results.head": "Search Results",
|
// "search.results.head": "Search Results",
|
||||||
@@ -4486,7 +4505,11 @@
|
|||||||
// "search.results.empty": "Your search returned no results.",
|
// "search.results.empty": "Your search returned no results.",
|
||||||
"search.results.empty": "Ihre Suche führte zu keinem Ergebnis.",
|
"search.results.empty": "Ihre Suche führte zu keinem Ergebnis.",
|
||||||
|
|
||||||
|
// "search.results.view-result": "View",
|
||||||
|
"search.results.view-result": "Anzeigen",
|
||||||
|
|
||||||
|
// "default.search.results.head": "Search Results",
|
||||||
|
"default.search.results.head": "Suchergebnisse",
|
||||||
|
|
||||||
// "search.sidebar.close": "Back to results",
|
// "search.sidebar.close": "Back to results",
|
||||||
"search.sidebar.close": "Zurück zu den Ergebnissen",
|
"search.sidebar.close": "Zurück zu den Ergebnissen",
|
||||||
@@ -4534,10 +4557,29 @@
|
|||||||
// "sorting.dc.title.DESC": "Title Descending",
|
// "sorting.dc.title.DESC": "Title Descending",
|
||||||
"sorting.dc.title.DESC": "Titel absteigend",
|
"sorting.dc.title.DESC": "Titel absteigend",
|
||||||
|
|
||||||
// "sorting.score.DESC": "Relevance",
|
// "sorting.score.ASC": "Least Relevant",
|
||||||
"sorting.score.DESC": "Relevanz",
|
"sorting.score.ASC": "Relevanz aufsteigend",
|
||||||
|
|
||||||
|
// "sorting.score.DESC": "Most Relevant",
|
||||||
|
"sorting.score.DESC": "Relevanz absteigend",
|
||||||
|
|
||||||
|
// "sorting.dc.date.issued.ASC": "Date Issued Ascending",
|
||||||
|
"sorting.dc.date.issued.ASC": "Erscheinungsdatum aufsteigend",
|
||||||
|
|
||||||
|
// "sorting.dc.date.issued.DESC": "Date Issued Descending",
|
||||||
|
"sorting.dc.date.issued.DESC": "Erscheinungsdatum absteigend",
|
||||||
|
|
||||||
|
// "sorting.dc.date.accessioned.ASC": "Accessioned Date Ascending",
|
||||||
|
"sorting.dc.date.accessioned.ASC": "Freischaltungsdatum aufsteigend",
|
||||||
|
|
||||||
|
// "sorting.dc.date.accessioned.DESC": "Accessioned Date Descending",
|
||||||
|
"sorting.dc.date.accessioned.DESC": "Freischaltungsdatum absteigend",
|
||||||
|
|
||||||
|
// "sorting.lastModified.ASC": "Last modified Ascending",
|
||||||
|
"sorting.lastModified.ASC": "Änderungsdatum aufsteigend",
|
||||||
|
|
||||||
|
// "sorting.lastModified.DESC": "Last modified Descending",
|
||||||
|
"sorting.lastModified.DESC": "Änderungsdatum absteigend",
|
||||||
|
|
||||||
// "statistics.title": "Statistics",
|
// "statistics.title": "Statistics",
|
||||||
"statistics.title": "Statistiken",
|
"statistics.title": "Statistiken",
|
||||||
@@ -5238,6 +5280,30 @@
|
|||||||
"submission.workflow.tasks.pool.show-detail": "Details anzeigen",
|
"submission.workflow.tasks.pool.show-detail": "Details anzeigen",
|
||||||
|
|
||||||
|
|
||||||
|
// "thumbnail.default.alt": "Thumbnail Image",
|
||||||
|
"thumbnail.default.alt": "Vorschaubild",
|
||||||
|
|
||||||
|
// "thumbnail.default.placeholder": "No Thumbnail Available",
|
||||||
|
"thumbnail.default.placeholder": "Vorschaubild nicht verfügbar",
|
||||||
|
|
||||||
|
// "thumbnail.project.alt": "Project Logo",
|
||||||
|
"thumbnail.project.alt": "Logo des Projekt",
|
||||||
|
|
||||||
|
// "thumbnail.project.placeholder": "Project Placeholder Image",
|
||||||
|
"thumbnail.project.placeholder": "Platzhalterbild des Projekts",
|
||||||
|
|
||||||
|
// "thumbnail.orgunit.alt": "OrgUnit Logo",
|
||||||
|
"thumbnail.orgunit.alt": "Logo der Organisationseinheit",
|
||||||
|
|
||||||
|
// "thumbnail.orgunit.placeholder": "OrgUnit Placeholder Image",
|
||||||
|
"thumbnail.orgunit.placeholder": "Platzhalterbild der Organisationseinheit",
|
||||||
|
|
||||||
|
// "thumbnail.person.alt": "Profile Picture",
|
||||||
|
"thumbnail.person.alt": "Profilbild",
|
||||||
|
|
||||||
|
// "thumbnail.person.placeholder": "No Profile Picture Available",
|
||||||
|
"thumbnail.person.placeholder": "Profilbild nicht verfügbar",
|
||||||
|
|
||||||
|
|
||||||
// "title": "DSpace",
|
// "title": "DSpace",
|
||||||
"title": "DSpace",
|
"title": "DSpace",
|
||||||
|
@@ -872,6 +872,12 @@
|
|||||||
|
|
||||||
"collection.edit.tabs.authorizations.title": "Collection Edit - Authorizations",
|
"collection.edit.tabs.authorizations.title": "Collection Edit - Authorizations",
|
||||||
|
|
||||||
|
"collection.edit.item.authorizations.load-bundle-button": "Load more bundles",
|
||||||
|
|
||||||
|
"collection.edit.item.authorizations.load-more-button": "Load more",
|
||||||
|
|
||||||
|
"collection.edit.item.authorizations.show-bitstreams-button": "Show bitstream policies for bundle",
|
||||||
|
|
||||||
"collection.edit.tabs.metadata.head": "Edit Metadata",
|
"collection.edit.tabs.metadata.head": "Edit Metadata",
|
||||||
|
|
||||||
"collection.edit.tabs.metadata.title": "Collection Edit - Metadata",
|
"collection.edit.tabs.metadata.title": "Collection Edit - Metadata",
|
||||||
@@ -2150,6 +2156,10 @@
|
|||||||
|
|
||||||
"item.search.title": "Item Search",
|
"item.search.title": "Item Search",
|
||||||
|
|
||||||
|
"item.truncatable-part.show-more": "Show more",
|
||||||
|
|
||||||
|
"item.truncatable-part.show-less": "Collapse",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"item.page.abstract": "Abstract",
|
"item.page.abstract": "Abstract",
|
||||||
|
@@ -107,7 +107,7 @@
|
|||||||
"admin.registries.bitstream-formats.edit.head": "Tiedostoformaatti: {{ format }}",
|
"admin.registries.bitstream-formats.edit.head": "Tiedostoformaatti: {{ format }}",
|
||||||
|
|
||||||
// "admin.registries.bitstream-formats.edit.internal.hint": "Formats marked as internal are hidden from the user, and used for administrative purposes.",
|
// "admin.registries.bitstream-formats.edit.internal.hint": "Formats marked as internal are hidden from the user, and used for administrative purposes.",
|
||||||
"admin.registries.bitstream-formats.edit.internal.hint": "Sisäisiksi merkittyjä formaatteja käytetään hallinnollisiin tarkoituksiin, ja ne on piilotettu käyttäjiltä.",
|
"admin.registries.bitstream-formats.edit.internal.hint": "Sisäisiksi merkittyjä formaatteja käytetään ylläpitotarkoituksiin, ja ne on piilotettu käyttäjiltä.",
|
||||||
|
|
||||||
// "admin.registries.bitstream-formats.edit.internal.label": "Internal",
|
// "admin.registries.bitstream-formats.edit.internal.label": "Internal",
|
||||||
"admin.registries.bitstream-formats.edit.internal.label": "Sisäinen",
|
"admin.registries.bitstream-formats.edit.internal.label": "Sisäinen",
|
||||||
@@ -662,7 +662,7 @@
|
|||||||
|
|
||||||
|
|
||||||
// "admin.search.breadcrumbs": "Administrative Search",
|
// "admin.search.breadcrumbs": "Administrative Search",
|
||||||
"admin.search.breadcrumbs": "Hallinnollinen haku",
|
"admin.search.breadcrumbs": "Ylläpitäjän haku",
|
||||||
|
|
||||||
// "admin.search.collection.edit": "Edit",
|
// "admin.search.collection.edit": "Edit",
|
||||||
"admin.search.collection.edit": "Muokkaa",
|
"admin.search.collection.edit": "Muokkaa",
|
||||||
@@ -692,19 +692,19 @@
|
|||||||
"admin.search.item.withdraw": "Poista käytöstä",
|
"admin.search.item.withdraw": "Poista käytöstä",
|
||||||
|
|
||||||
// "admin.search.title": "Administrative Search",
|
// "admin.search.title": "Administrative Search",
|
||||||
"admin.search.title": "Hallinnollinen haku",
|
"admin.search.title": "Ylläpitäjän haku",
|
||||||
|
|
||||||
// "administrativeView.search.results.head": "Administrative Search",
|
// "administrativeView.search.results.head": "Administrative Search",
|
||||||
"administrativeView.search.results.head": "Hallinnollinen haku",
|
"administrativeView.search.results.head": "Ylläpitäjän haku",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// "admin.workflow.breadcrumbs": "Administer Workflow",
|
// "admin.workflow.breadcrumbs": "Administer Workflow",
|
||||||
"admin.workflow.breadcrumbs": "Hallinnointityönkulku",
|
"admin.workflow.breadcrumbs": "Hallinnoi työnkulkua",
|
||||||
|
|
||||||
// "admin.workflow.title": "Administer Workflow",
|
// "admin.workflow.title": "Administer Workflow",
|
||||||
"admin.workflow.title": "Hallinnointityönkulku",
|
"admin.workflow.title": "Hallinnoi työnkulkua",
|
||||||
|
|
||||||
// "admin.workflow.item.workflow": "Workflow",
|
// "admin.workflow.item.workflow": "Workflow",
|
||||||
"admin.workflow.item.workflow": "Työnkulku",
|
"admin.workflow.item.workflow": "Työnkulku",
|
||||||
@@ -2954,7 +2954,7 @@
|
|||||||
|
|
||||||
|
|
||||||
// "menu.section.admin_search": "Admin Search",
|
// "menu.section.admin_search": "Admin Search",
|
||||||
"menu.section.admin_search": "Admin-haku",
|
"menu.section.admin_search": "Ylläpitäjän haku",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -3033,7 +3033,7 @@
|
|||||||
"menu.section.icon.access_control": "Pääsyoikeudet",
|
"menu.section.icon.access_control": "Pääsyoikeudet",
|
||||||
|
|
||||||
// "menu.section.icon.admin_search": "Admin search menu section",
|
// "menu.section.icon.admin_search": "Admin search menu section",
|
||||||
"menu.section.icon.admin_search": "Admin-haku",
|
"menu.section.icon.admin_search": "Ylläpitäjän haku",
|
||||||
|
|
||||||
// "menu.section.icon.control_panel": "Control Panel menu section",
|
// "menu.section.icon.control_panel": "Control Panel menu section",
|
||||||
"menu.section.icon.control_panel": "Hallintapaneeli",
|
"menu.section.icon.control_panel": "Hallintapaneeli",
|
||||||
@@ -3168,7 +3168,7 @@
|
|||||||
|
|
||||||
|
|
||||||
// "menu.section.workflow": "Administer Workflow",
|
// "menu.section.workflow": "Administer Workflow",
|
||||||
"menu.section.workflow": "Hallinnointityönkulku",
|
"menu.section.workflow": "Hallinnoi työnkulkua",
|
||||||
|
|
||||||
|
|
||||||
// "mydspace.description": "",
|
// "mydspace.description": "",
|
||||||
@@ -5079,7 +5079,7 @@
|
|||||||
|
|
||||||
|
|
||||||
// "workflowAdmin.search.results.head": "Administer Workflow",
|
// "workflowAdmin.search.results.head": "Administer Workflow",
|
||||||
"workflowAdmin.search.results.head": "Hallinnointityönkulku",
|
"workflowAdmin.search.results.head": "Hallinnoi työnkulkua",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user