Merge remote-tracking branch 'origin/main' into CST-5668

This commit is contained in:
Giuseppe Digilio
2022-06-16 09:13:01 +02:00
23 changed files with 541 additions and 157 deletions

View File

@@ -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"/> \

View File

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

View File

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

View File

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

View File

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

View File

@@ -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">
<div class="card auth-bitstream-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>
</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>

View File

@@ -0,0 +1,4 @@
.auth-bitstream-container {
margin-top: -1em;
margin-bottom: 1.5em;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
.content:not(.truncated) ~ button.expandButton {
display: none;
}
.btn:focus {
box-shadow: none !important;
}
.removeFaded.content::after {
display: none;
}

View File

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

View File

@@ -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
*/ */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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