Merge branch 'master' into metadata-and-relationships-combined-in-submission

This commit is contained in:
Art Lowel
2020-04-01 14:06:06 +02:00
283 changed files with 11336 additions and 3579 deletions

28
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,28 @@
## References
_Add references/links to any related tickets or PRs. These may include:_
* Link to [Angular issue or PR](https://github.com/DSpace/dspace-angular/issues) related to this PR, if any
* Link to [JIRA](https://jira.lyrasis.org/projects/DS/summary) ticket(s), if any
## Description
Short summary of changes (1-2 sentences).
## Instructions for Reviewers
Please add a more detailed description of the changes made by your PR. At a minimum, providing a bulleted list of changes in your PR is helpful to reviewers.
List of changes in this PR:
* First, ...
* Second, ...
**Include guidance for how to test or review your PR.** This may include: steps to reproduce a bug, screenshots or description of a new feature, or reasons behind specific changes.
## Checklist
_This checklist provides a reminder of what we are going to look for when reviewing your PR. You need not complete this checklist prior to creating your PR (draft PRs are always welcome). If you are unsure about an item in the checklist, don't hesitate to ask. We're here to help!_
- [ ] My PR is small in size (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible.
- [ ] My PR passes [TSLint](https://palantir.github.io/tslint/) validation using `yarn run lint`
- [ ] My PR includes [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods.
- [ ] My PR passes all specs/tests and includes new/updated specs for any bug fixes, improvements or new features. A few reminders about what constitutes good tests:
* Include tests for different user types (if behavior differs), including: (1) Anonymous user, (2) Logged in user (non-admin), and (3) Administrator.
* Include tests for error scenarios, e.g. when errors/warnings should appear (or buttons should be disabled).
* For bug fixes, include a test that reproduces the bug and proves it is fixed. For clarity, it may be useful to provide the test in a separate commit from the bug fix.
- [ ] If my PR includes new, third-party dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/master/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.

2
.gitignore vendored
View File

@@ -34,3 +34,5 @@ yarn-error.log
*.css
package-lock.json
.java-version

View File

@@ -6,5 +6,7 @@ WORKDIR /app
ADD . /app/
EXPOSE 3000
RUN yarn install
# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com
# See, for example https://github.com/yarnpkg/yarn/issues/5540
RUN yarn install --network-timeout 300000
CMD yarn run watch

39
LICENSE Normal file
View File

@@ -0,0 +1,39 @@
DSpace source code BSD License:
Copyright (c) 2002-2020, LYRASIS. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
- Neither the name DuraSpace nor the name of the DSpace Foundation
nor the names of its contributors may be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
DSpace uses third-party libraries which may be distributed under
different licenses to the above. Information about these licenses
is detailed in the LICENSES_THIRD_PARTY file at the root of the source
tree. You must agree to the terms of these licenses, in addition to
the above DSpace source code license, in order to use this software.

15
LICENSES_THIRD_PARTY Normal file
View File

@@ -0,0 +1,15 @@
DSpace uses third-party libraries which may be distributed under different licenses.
A summary of all third-party, production dependencies used by this user interface may be found by running:
npx license-checker --production --summary
(Additional license-checker options may be found in its documentation: https://github.com/davglass/license-checker)
You must agree to the terms of these licenses, in addition to the DSpace source code license, in order to use this
software.
PLEASE NOTE: Some third-party dependencies may be listed under multiple licenses if they are dual-licensed.
This is especially true of anything listed as GPL (or similar), as DSpace does NOT allow for the inclusion of
any dependencies that are solely released under GPL (or similar) terms. For more info see:
https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions

View File

@@ -140,7 +140,7 @@ module.exports = {
}, {
code: 'nl',
label: 'Nederlands',
active: false,
active: true,
}, {
code: 'pt',
label: 'Português',

View File

@@ -170,6 +170,98 @@
"admin.access-control.epeople.title": "DSpace Angular :: EPeople",
"admin.access-control.epeople.head": "EPeople",
"admin.access-control.epeople.search.head": "Search",
"admin.access-control.epeople.search.scope.name": "Name",
"admin.access-control.epeople.search.scope.email": "E-mail (exact)",
"admin.access-control.epeople.search.scope.metadata": "Metadata",
"admin.access-control.epeople.search.button": "Search",
"admin.access-control.epeople.button.add": "Add EPerson",
"admin.access-control.epeople.table.id": "ID",
"admin.access-control.epeople.table.name": "Name",
"admin.access-control.epeople.table.email": "E-mail",
"admin.access-control.epeople.table.edit": "Edit",
"item.access-control.epeople.table.edit.buttons.edit": "Edit",
"item.access-control.epeople.table.edit.buttons.remove": "Remove",
"admin.access-control.epeople.no-items": "No EPeople to show.",
"admin.access-control.epeople.form.create": "Create EPerson",
"admin.access-control.epeople.form.edit": "Edit EPerson",
"admin.access-control.epeople.form.firstName": "First name",
"admin.access-control.epeople.form.lastName": "Last name",
"admin.access-control.epeople.form.email": "E-mail",
"admin.access-control.epeople.form.emailHint": "Must be valid e-mail address",
"admin.access-control.epeople.form.canLogIn": "Can log in",
"admin.access-control.epeople.form.requireCertificate": "Requires certificate",
"admin.access-control.epeople.form.notification.created.success": "Successfully created EPerson \"{{name}}\"",
"admin.access-control.epeople.form.notification.created.failure": "Failed to create EPerson \"{{name}}\"",
"admin.access-control.epeople.form.notification.created.failure.emailInUse": "Failed to create EPerson \"{{name}}\", email \"{{email}}\" already in use.",
"admin.access-control.epeople.form.notification.edited.failure.emailInUse": "Failed to edit EPerson \"{{name}}\", email \"{{email}}\" already in use.",
"admin.access-control.epeople.form.notification.edited.success": "Successfully edited EPerson \"{{name}}\"",
"admin.access-control.epeople.form.notification.edited.failure": "Failed to edit EPerson \"{{name}}\"",
"admin.access-control.epeople.notification.deleted.failure": "Failed to delete EPerson: \"{{name}}\"",
"admin.access-control.epeople.notification.deleted.success": "Successfully deleted EPerson: \"{{name}}\"",
"admin.search.breadcrumbs": "Administrative Search",
"admin.search.collection.edit": "Edit",
"admin.search.community.edit": "Edit",
"admin.search.item.delete": "Delete",
"admin.search.item.edit": "Edit",
"admin.search.item.make-private": "Make Private",
"admin.search.item.make-public": "Make Public",
"admin.search.item.move": "Move",
"admin.search.item.private": "Private",
"admin.search.item.reinstate": "Reinstate",
"admin.search.item.withdraw": "Withdraw",
"admin.search.item.withdrawn": "Withdrawn",
"admin.search.title": "Administrative Search",
"auth.errors.invalid-user": "Invalid email address or password.",
"auth.messages.expired": "Your session has expired. Please log in again.",
@@ -196,6 +288,14 @@
"browse.metadata.title": "Title",
"browse.metadata.author.breadcrumbs": "Browse by Author",
"browse.metadata.dateissued.breadcrumbs": "Browse by Date",
"browse.metadata.subject.breadcrumbs": "Browse by Subject",
"browse.metadata.title.breadcrumbs": "Browse by Title",
"browse.startsWith.choose_start": "(Choose start)",
"browse.startsWith.choose_year": "(Choose year)",
@@ -237,7 +337,6 @@
"browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}",
"chips.remove": "Remove chip",
@@ -266,6 +365,8 @@
"collection.edit.head": "Edit Collection",
"collection.edit.breadcrumbs": "Edit Collection",
"collection.edit.item-mapper.cancel": "Cancel",
@@ -450,6 +551,7 @@
"community.edit.head": "Edit Community",
"community.edit.breadcrumbs": "Edit Community",
"community.edit.logo.label": "Community logo",
@@ -657,6 +759,8 @@
"item.edit.head": "Edit Item",
"item.edit.breadcrumbs": "Edit Item",
"item.edit.item-mapper.buttons.add": "Map item to selected collections",
@@ -901,6 +1005,12 @@
"item.edit.tabs.status.title": "Item Edit - Status",
"item.edit.tabs.versionhistory.head": "Version History",
"item.edit.tabs.versionhistory.title": "Item Edit - Version History",
"item.edit.tabs.versionhistory.under-construction": "Editing or adding new versions is not yet possible in this user interface.",
"item.edit.tabs.view.head": "View Item",
"item.edit.tabs.view.title": "Item Edit - View",
@@ -980,6 +1090,29 @@
"item.select.table.title": "Title",
"item.version.history.empty": "There are no other versions for this item yet.",
"item.version.history.head": "Version History",
"item.version.history.return": "Return",
"item.version.history.selected": "Selected version",
"item.version.history.table.version": "Version",
"item.version.history.table.item": "Item",
"item.version.history.table.editor": "Editor",
"item.version.history.table.date": "Date",
"item.version.history.table.summary": "Summary",
"item.version.notice": "This is not the latest version of this item. The latest version can be found <a href='{{destination}}'>here</a>.",
"journal.listelement.badge": "Journal",
@@ -1071,12 +1204,18 @@
"login.form.new-user": "New user? Click here to register.",
"login.form.or-divider": "or",
"login.form.password": "Password",
"login.form.shibboleth": "Log in with Shibboleth",
"login.form.submit": "Log in",
"login.title": "Login",
"login.breadcrumbs": "Login",
"logout.form.header": "Log out from DSpace",
@@ -1103,6 +1242,10 @@
"menu.section.admin_search": "Admin Search",
"menu.section.browse_community": "This Community",
"menu.section.browse_community_by_author": "By Author",
@@ -1153,18 +1296,10 @@
"menu.section.find": "Find",
"menu.section.find_items": "Items",
"menu.section.find_private_items": "Private Items",
"menu.section.find_withdrawn_items": "Withdrawn Items",
"menu.section.icon.access_control": "Access Control menu section",
"menu.section.icon.admin_search": "Admin search menu section",
"menu.section.icon.control_panel": "Control Panel menu section",
"menu.section.icon.curation_task": "Curation Task menu section",
@@ -1339,6 +1474,8 @@
"nav.mydspace": "MyDSpace",
"nav.profile": "Profile",
"nav.search": "Search",
"nav.statistics.header": "Statistics",
@@ -1397,6 +1534,64 @@
"profile.breadcrumbs": "Update Profile",
"profile.card.identify": "Identify",
"profile.card.security": "Security",
"profile.form.submit": "Update Profile",
"profile.groups.head": "Authorization groups you belong to",
"profile.head": "Update Profile",
"profile.metadata.form.error.firstname.required": "First Name is required",
"profile.metadata.form.error.lastname.required": "Last Name is required",
"profile.metadata.form.label.email": "Email Address",
"profile.metadata.form.label.firstname": "First Name",
"profile.metadata.form.label.language": "Language",
"profile.metadata.form.label.lastname": "Last Name",
"profile.metadata.form.label.phone": "Contact Telephone",
"profile.metadata.form.notifications.success.content": "Your changes to the profile were saved.",
"profile.metadata.form.notifications.success.title": "Profile saved",
"profile.notifications.warning.no-changes.content": "No changes were made to the Profile.",
"profile.notifications.warning.no-changes.title": "No changes",
"profile.security.form.error.matching-passwords": "The passwords do not match.",
"profile.security.form.error.password-length": "The password should be at least 6 characters long.",
"profile.security.form.info": "Optionally, you can enter a new password in the box below, and confirm it by typing it again into the second box. It should be at least six characters long.",
"profile.security.form.label.password": "Password",
"profile.security.form.label.passwordrepeat": "Retype to confirm",
"profile.security.form.notifications.success.content": "Your changes to the password were saved.",
"profile.security.form.notifications.success.title": "Password saved",
"profile.security.form.notifications.error.title": "Error changing passwords",
"profile.security.form.notifications.error.not-long-enough": "The password has to be at least 6 characters long.",
"profile.security.form.notifications.error.not-same": "The provided passwords are not the same.",
"profile.title": "Update Profile",
"project.listelement.badge": "Research Project",
"project.page.contributor": "Contributors",
@@ -1473,6 +1668,7 @@
"search.title": "DSpace Angular :: Search",
"search.breadcrumbs": "Search",
"search.filters.applied.f.author": "Author",
@@ -1483,6 +1679,8 @@
"search.filters.applied.f.dateSubmitted": "Date submitted",
"search.filters.applied.f.discoverable": "Private",
"search.filters.applied.f.entityType": "Item Type",
"search.filters.applied.f.has_content_in_original_bundle": "Has files",
@@ -1494,10 +1692,15 @@
"search.filters.applied.f.subject": "Subject",
"search.filters.applied.f.submitter": "Submitter",
"search.filters.applied.f.jobTitle": "Job Title",
"search.filters.applied.f.birthDate.max": "End birth date",
"search.filters.applied.f.birthDate.min": "Start birth date",
"search.filters.applied.f.withdrawn": "Withdrawn",
"search.filters.filter.author.head": "Author",
@@ -1534,6 +1737,10 @@
"search.filters.filter.dateSubmitted.placeholder": "Date submitted",
"search.filters.filter.discoverable.head": "Private",
"search.filters.filter.withdrawn.head": "Withdrawn",
"search.filters.filter.entityType.head": "Item Type",
"search.filters.filter.entityType.placeholder": "Item Type",
@@ -1590,6 +1797,25 @@
"search.filters.entityType.JournalIssue": "Journal Issue",
"search.filters.entityType.JournalVolume": "Journal Volume",
"search.filters.entityType.OrgUnit": "Organizational Unit",
"search.filters.has_content_in_original_bundle.true": "Yes",
"search.filters.has_content_in_original_bundle.false": "No",
"search.filters.discoverable.true": "No",
"search.filters.discoverable.false": "Yes",
"search.filters.withdrawn.true": "Yes",
"search.filters.withdrawn.false": "No",
"search.filters.head": "Filters",
"search.filters.reset": "Reset filters",
@@ -1973,6 +2199,8 @@
"title": "DSpace",
"administrativeView.search.results.head": "Administrative Search",
"uploader.browse": "browse",

File diff suppressed because it is too large Load Diff

View File

@@ -918,7 +918,7 @@
"item.edit.move.processing": "Movendo...",
// "item.edit.move.search.placeholder": "Enter a search query to look for collections",
"item.edit.move.search.placeholder": "nsira uma consulta para procurar coleções",
"item.edit.move.search.placeholder": "Insira uma consulta para procurar coleções",
// "item.edit.move.success": "The item has been moved successfully",
"item.edit.move.success": "O item foi movido com sucesso",

View File

@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
@NgModule({
imports: [
RouterModule.forChild([
{ path: 'epeople', component: EPeopleRegistryComponent, data: { title: 'admin.access-control.epeople.title' } },
])
]
})
/**
* Routing module for the AccessControl section of the admin sidebar
*/
export class AdminAccessControlRoutingModule {
}

View File

@@ -0,0 +1,29 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { SharedModule } from '../../shared/shared.module';
import { AdminAccessControlRoutingModule } from './admin-access-control-routing.module';
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
@NgModule({
imports: [
CommonModule,
SharedModule,
RouterModule,
TranslateModule,
AdminAccessControlRoutingModule
],
declarations: [
EPeopleRegistryComponent,
EPersonFormComponent
],
entryComponents: []
})
/**
* This module handles all components related to the access control pages
*/
export class AdminAccessControlModule {
}

View File

@@ -0,0 +1,49 @@
import { Action } from '@ngrx/store';
import { EPerson } from '../../../core/eperson/models/eperson.model';
import { type } from '../../../shared/ngrx/type';
/**
* For each action type in an action group, make a simple
* enum object for all of this group's action types.
*
* The 'type' utility function coerces strings into string
* literal types and runs a simple check to guarantee all
* action types in the application are unique.
*/
export const EPeopleRegistryActionTypes = {
EDIT_EPERSON: type('dspace/epeople-registry/EDIT_EPERSON'),
CANCEL_EDIT_EPERSON: type('dspace/epeople-registry/CANCEL_EDIT_EPERSON'),
};
/* tslint:disable:max-classes-per-file */
/**
* Used to edit an EPerson in the EPeople registry
*/
export class EPeopleRegistryEditEPersonAction implements Action {
type = EPeopleRegistryActionTypes.EDIT_EPERSON;
eperson: EPerson;
constructor(eperson: EPerson) {
this.eperson = eperson;
}
}
/**
* Used to cancel the editing of an EPerson in the EPeople registry
*/
export class EPeopleRegistryCancelEPersonAction implements Action {
type = EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON;
}
/* tslint:enable:max-classes-per-file */
/**
* Export a type alias of all actions in this action group
* so that reducers can easily compose action types
* These are all the actions to perform on the EPeople registry state
*/
export type EPeopleRegistryAction
= EPeopleRegistryEditEPersonAction
| EPeopleRegistryCancelEPersonAction

View File

@@ -0,0 +1,90 @@
<div class="container">
<div class="epeople-registry row">
<div class="col-12">
<h2 id="header" class="border-bottom pb-2">{{labelPrefix + 'head' | translate}}</h2>
<ds-eperson-form *ngIf="isEPersonFormShown" (submitForm)="forceUpdateEPeople()"
(cancelForm)="isEPersonFormShown = false"></ds-eperson-form>
<div *ngIf="!isEPersonFormShown" class="button-row top d-flex pb-2">
<button class="mr-auto btn btn-success addEPerson-button"
(click)="isEPersonFormShown = true">
<i class="fas fa-plus"></i>
<span class="d-none d-sm-inline">{{labelPrefix + 'button.add' | translate}}</span>
</button>
</div>
<h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | translate}}</h3>
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row">
<div class="col-12 col-sm-3">
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
<option value="metadata">{{labelPrefix + 'search.scope.metadata' | translate}}</option>
<option value="email">{{labelPrefix + 'search.scope.email' | translate}}</option>
</select>
</div>
<div class="col-sm-9 col-12">
<div class="form-group input-group">
<input type="text" name="query" id="query" formControlName="query"
class="form-control" aria-label="Search input">
<span class="input-group-append">
<button type="submit"
class="search-button btn btn-secondary">{{ labelPrefix + 'search.button' | translate }}</button>
</span>
</div>
</div>
</form>
<ds-pagination
*ngIf="(ePeople | async)?.payload?.totalElements > 0"
[paginationOptions]="config"
[pageInfoState]="(ePeople | async)?.payload"
[collectionSize]="(ePeople | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(pageChange)="onPageChange($event)">
<div class="table-responsive">
<table id="epeople" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col">{{labelPrefix + 'table.id' | translate}}</th>
<th scope="col">{{labelPrefix + 'table.name' | translate}}</th>
<th scope="col">{{labelPrefix + 'table.email' | translate}}</th>
<th>{{labelPrefix + 'table.edit' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let eperson of (ePeople | async)?.payload?.page"
[ngClass]="{'table-primary' : isActive(eperson) | async}">
<td>{{eperson.id}}</td>
<td>{{eperson.name}}</td>
<td>{{eperson.email}}</td>
<td>
<div class="btn-group edit-field">
<button (click)="toggleEditEPerson(eperson)"
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.edit' | translate}}">
<i class="fas fa-edit fa-fw"></i>
</button>
<button (click)="deleteEPerson(eperson)"
class="btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.remove' | translate}}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</ds-pagination>
<div *ngIf="(ePeople | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
{{labelPrefix + 'no-items' | translate}}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,205 @@
import { of as observableOf } from 'rxjs';
import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs/internal/Observable';
import { PaginatedList } from '../../../core/data/paginated-list';
import { RemoteData } from '../../../core/data/remote-data';
import { FindListOptions } from '../../../core/data/request.models';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model';
import { PageInfo } from '../../../core/shared/page-info.model';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
import { getMockFormBuilderService } from '../../../shared/mocks/mock-form-builder-service';
import { MockTranslateLoader } from '../../../shared/mocks/mock-translate-loader';
import { getMockTranslateService } from '../../../shared/mocks/mock-translate.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson-mock';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { EPeopleRegistryComponent } from './epeople-registry.component';
describe('EPeopleRegistryComponent', () => {
let component: EPeopleRegistryComponent;
let fixture: ComponentFixture<EPeopleRegistryComponent>;
let translateService: TranslateService;
let builderService: FormBuilderService;
const mockEPeople = [EPersonMock, EPersonMock2];
let ePersonDataServiceStub: any;
beforeEach(async(() => {
ePersonDataServiceStub = {
activeEPerson: null,
allEpeople: mockEPeople,
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
},
getActiveEPerson(): Observable<EPerson> {
return observableOf(this.activeEPerson);
},
searchByScope(scope: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<EPerson>>> {
if (scope === 'email') {
const result = this.allEpeople.find((ePerson: EPerson) => {
return ePerson.email === query
});
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
}
if (scope === 'metadata') {
const result = this.allEpeople.find((ePerson: EPerson) => {
return (ePerson.name.includes(query) || ePerson.email.includes(query))
});
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
}
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
},
deleteEPerson(ePerson: EPerson): Observable<boolean> {
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
return (ePerson2.uuid !== ePerson.uuid);
});
return observableOf(true);
},
editEPerson(ePerson: EPerson) {
this.activeEPerson = ePerson;
},
cancelEditEPerson() {
this.activeEPerson = null;
},
clearEPersonRequests(): void {
// empty
}
};
builderService = getMockFormBuilderService();
translateService = getMockTranslateService();
TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
}),
],
declarations: [EPeopleRegistryComponent],
providers: [EPeopleRegistryComponent,
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: FormBuilderService, useValue: builderService },
EPeopleRegistryComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EPeopleRegistryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create EPeopleRegistryComponent', inject([EPeopleRegistryComponent], (comp: EPeopleRegistryComponent) => {
expect(comp).toBeDefined();
}));
it('should display list of ePeople', () => {
const ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
expect(ePeopleIdsFound.length).toEqual(2);
mockEPeople.map((ePerson: EPerson) => {
expect(ePeopleIdsFound.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === ePerson.uuid);
})).toBeTruthy();
})
});
describe('search', () => {
describe('when searching with scope/query (scope metadata)', () => {
let ePeopleIdsFound;
beforeEach(fakeAsync(() => {
component.search({ scope: 'metadata', query: EPersonMock2.name });
tick();
fixture.detectChanges();
ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
}));
it('should display search result', () => {
expect(ePeopleIdsFound.length).toEqual(1);
expect(ePeopleIdsFound.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === EPersonMock2.uuid);
})).toBeTruthy();
});
});
describe('when searching with scope/query (scope email)', () => {
let ePeopleIdsFound;
beforeEach(fakeAsync(() => {
component.search({ scope: 'email', query: EPersonMock.email });
tick();
fixture.detectChanges();
ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
}));
it('should display search result', () => {
expect(ePeopleIdsFound.length).toEqual(1);
expect(ePeopleIdsFound.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === EPersonMock.uuid);
})).toBeTruthy();
});
});
});
describe('toggleEditEPerson', () => {
describe('when you click on first edit eperson button', () => {
beforeEach(fakeAsync(() => {
const editButtons = fixture.debugElement.queryAll(By.css('.access-control-editEPersonButton'));
editButtons[0].triggerEventHandler('click', {
preventDefault: () => {/**/
}
});
tick();
fixture.detectChanges();
}));
it('editEPerson form is toggled', () => {
const ePeopleIds = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
ePersonDataServiceStub.getActiveEPerson().subscribe((activeEPerson: EPerson) => {
if (activeEPerson === ePeopleIds[0].nativeElement.textContent) {
expect(component.isEPersonFormShown).toEqual(false);
} else {
expect(component.isEPersonFormShown).toEqual(true);
}
})
});
});
});
describe('deleteEPerson', () => {
describe('when you click on first delete eperson button', () => {
let ePeopleIdsFoundBeforeDelete;
let ePeopleIdsFoundAfterDelete;
beforeEach(fakeAsync(() => {
ePeopleIdsFoundBeforeDelete = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
deleteButtons[0].triggerEventHandler('click', {
preventDefault: () => {/**/
}
});
tick();
fixture.detectChanges();
ePeopleIdsFoundAfterDelete = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
}));
it('first ePerson is deleted', () => {
expect(ePeopleIdsFoundBeforeDelete.length === ePeopleIdsFoundAfterDelete + 1);
ePeopleIdsFoundAfterDelete.forEach((epersonElement) => {
expect(epersonElement !== ePeopleIdsFoundBeforeDelete[0].nativeElement.textContent).toBeTrue();
});
});
});
});
});

View File

@@ -0,0 +1,163 @@
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { PaginatedList } from '../../../core/data/paginated-list';
import { RemoteData } from '../../../core/data/remote-data';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model';
import { hasValue } from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
@Component({
selector: 'ds-epeople-registry',
templateUrl: './epeople-registry.component.html',
})
/**
* A component used for managing all existing epeople within the repository.
* The admin can create, edit or delete epeople here.
*/
export class EPeopleRegistryComponent {
labelPrefix = 'admin.access-control.epeople.';
/**
* A list of all the current EPeople within the repository or the result of the search
*/
ePeople: Observable<RemoteData<PaginatedList<EPerson>>>;
/**
* Pagination config used to display the list of epeople
*/
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'epeople-list-pagination',
pageSize: 5,
currentPage: 1
});
/**
* Whether or not to show the EPerson form
*/
isEPersonFormShown: boolean;
// The search form
searchForm;
constructor(private epersonService: EPersonDataService,
private translateService: TranslateService,
private notificationsService: NotificationsService,
private formBuilder: FormBuilder) {
this.updateEPeople({
currentPage: 1,
elementsPerPage: this.config.pageSize
});
this.isEPersonFormShown = false;
this.searchForm = this.formBuilder.group(({
scope: 'metadata',
query: '',
}));
}
/**
* Event triggered when the user changes page
* @param event
*/
onPageChange(event) {
this.updateEPeople({
currentPage: event,
elementsPerPage: this.config.pageSize
});
}
/**
* Update the list of EPeople by fetching it from the rest api or cache
*/
private updateEPeople(options) {
this.ePeople = this.epersonService.getEPeople(options);
}
/**
* Force-update the list of EPeople by first clearing the cache related to EPeople, then performing
* a new REST call
*/
public forceUpdateEPeople() {
this.epersonService.clearEPersonRequests();
this.isEPersonFormShown = false;
this.search({ query: '', scope: 'metadata' })
}
/**
* Search in the EPeople by metadata (default) or email
* @param data Contains scope and query param
*/
search(data: any) {
this.ePeople = this.epersonService.searchByScope(data.scope, data.query, {
currentPage: 1,
elementsPerPage: this.config.pageSize
});
}
/**
* Checks whether the given EPerson is active (being edited)
* @param eperson
*/
isActive(eperson: EPerson): Observable<boolean> {
return this.getActiveEPerson().pipe(
map((activeEPerson) => eperson === activeEPerson)
);
}
/**
* Gets the active eperson (being edited)
*/
getActiveEPerson(): Observable<EPerson> {
return this.epersonService.getActiveEPerson();
}
/**
* Start editing the selected EPerson
* @param ePerson
*/
toggleEditEPerson(ePerson: EPerson) {
this.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => {
if (ePerson === activeEPerson) {
this.epersonService.cancelEditEPerson();
this.isEPersonFormShown = false;
} else {
this.epersonService.editEPerson(ePerson);
this.isEPersonFormShown = true;
}
});
this.scrollToTop()
}
/**
* Deletes EPerson, show notification on success/failure & updates EPeople list
*/
deleteEPerson(ePerson: EPerson) {
if (hasValue(ePerson.id)) {
this.epersonService.deleteEPerson(ePerson).pipe(take(1)).subscribe((success: boolean) => {
if (success) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: ePerson.name }));
this.forceUpdateEPeople();
} else {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.failure', { name: ePerson.name }));
}
this.epersonService.cancelEditEPerson();
this.isEPersonFormShown = false;
})
}
}
scrollToTop() {
(function smoothscroll() {
const currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
if (currentScroll > 0) {
window.requestAnimationFrame(smoothscroll);
window.scrollTo(0, currentScroll - (currentScroll / 8));
}
})();
}
}

View File

@@ -0,0 +1,54 @@
import { EPersonMock } from '../../../shared/testing/eperson-mock';
import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from './epeople-registry.actions';
import { ePeopleRegistryReducer, EPeopleRegistryState } from './epeople-registry.reducers';
const initialState: EPeopleRegistryState = {
editEPerson: null,
};
const editState: EPeopleRegistryState = {
editEPerson: EPersonMock,
};
class NullAction extends EPeopleRegistryEditEPersonAction {
type = null;
constructor() {
super(undefined);
}
}
describe('epeopleRegistryReducer', () => {
it('should return the current state when no valid actions have been made', () => {
const state = initialState;
const action = new NullAction();
const newState = ePeopleRegistryReducer(state, action);
expect(newState).toEqual(state);
});
it('should start with an initial state', () => {
const state = initialState;
const action = new NullAction();
const initState = ePeopleRegistryReducer(undefined, action);
expect(initState).toEqual(state);
});
it('should update the current state to change the editEPerson to a new eperson when EPeopleRegistryEditEPersonAction is dispatched', () => {
const state = editState;
const action = new EPeopleRegistryEditEPersonAction(EPersonMock);
const newState = ePeopleRegistryReducer(state, action);
expect(newState.editEPerson).toEqual(EPersonMock);
});
it('should update the current state to remove the editEPerson from the state when EPeopleRegistryCancelEPersonAction is dispatched', () => {
const state = editState;
const action = new EPeopleRegistryCancelEPersonAction();
const newState = ePeopleRegistryReducer(state, action);
expect(newState.editEPerson).toEqual(null);
});
});

View File

@@ -0,0 +1,47 @@
import { EPerson } from '../../../core/eperson/models/eperson.model';
import {
EPeopleRegistryAction,
EPeopleRegistryActionTypes,
EPeopleRegistryEditEPersonAction
} from './epeople-registry.actions';
/**
* The EPeople registry state.
* @interface EPeopleRegistryState
*/
export interface EPeopleRegistryState {
editEPerson: EPerson;
}
/**
* The initial state.
*/
const initialState: EPeopleRegistryState = {
editEPerson: null,
};
/**
* Reducer that handles EPeopleRegistryActions to modify EPeople
* @param state The current EPeopleRegistryState
* @param action The EPeopleRegistryAction to perform on the state
*/
export function ePeopleRegistryReducer(state = initialState, action: EPeopleRegistryAction): EPeopleRegistryState {
switch (action.type) {
case EPeopleRegistryActionTypes.EDIT_EPERSON: {
return Object.assign({}, state, {
editEPerson: (action as EPeopleRegistryEditEPersonAction).eperson
});
}
case EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON: {
return Object.assign({}, state, {
editEPerson: null
});
}
default:
return state;
}
}

View File

@@ -0,0 +1,17 @@
<div *ngIf="epersonService.getActiveEPerson() | async; then editheader; else createHeader"></div>
<ng-template #createHeader>
<h4>{{messagePrefix + '.create' | translate}}</h4>
</ng-template>
<ng-template #editheader>
<h4>{{messagePrefix + '.edit' | translate}}</h4>
</ng-template>
<ds-form [formId]="formId"
[formModel]="formModel"
[formGroup]="formGroup"
[formLayout]="formLayout"
(cancel)="onCancel()"
(submitForm)="onSubmit()">
</ds-form>

View File

@@ -0,0 +1,206 @@
import { of as observableOf } from 'rxjs';
import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs/internal/Observable';
import { RestResponse } from '../../../../core/cache/response.models';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { RemoteData } from '../../../../core/data/remote-data';
import { FindListOptions } from '../../../../core/data/request.models';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
import { EPerson } from '../../../../core/eperson/models/eperson.model';
import { PageInfo } from '../../../../core/shared/page-info.model';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { getMockFormBuilderService } from '../../../../shared/mocks/mock-form-builder-service';
import { getMockTranslateService } from '../../../../shared/mocks/mock-translate.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson-mock';
import { MockTranslateLoader } from '../../../../shared/testing/mock-translate-loader';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
import { EPeopleRegistryComponent } from '../epeople-registry.component';
import { EPersonFormComponent } from './eperson-form.component';
describe('EPersonFormComponent', () => {
let component: EPersonFormComponent;
let fixture: ComponentFixture<EPersonFormComponent>;
let translateService: TranslateService;
let builderService: FormBuilderService;
const mockEPeople = [EPersonMock, EPersonMock2];
let ePersonDataServiceStub: any;
beforeEach(async(() => {
ePersonDataServiceStub = {
activeEPerson: null,
allEpeople: mockEPeople,
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
},
getActiveEPerson(): Observable<EPerson> {
return observableOf(this.activeEPerson);
},
searchByScope(scope: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<EPerson>>> {
if (scope === 'email') {
const result = this.allEpeople.find((ePerson: EPerson) => {
return ePerson.email === query
});
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
}
if (scope === 'metadata') {
const result = this.allEpeople.find((ePerson: EPerson) => {
return (ePerson.name.includes(query) || ePerson.email.includes(query))
});
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
}
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
},
deleteEPerson(ePerson: EPerson): Observable<boolean> {
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
return (ePerson2.uuid !== ePerson.uuid);
});
return observableOf(true);
},
create(ePerson: EPerson) {
this.allEpeople = [...this.allEpeople, ePerson]
},
editEPerson(ePerson: EPerson) {
this.activeEPerson = ePerson;
},
cancelEditEPerson() {
this.activeEPerson = null;
},
clearEPersonRequests(): void {
// empty
},
tryToCreate(ePerson: EPerson): Observable<RestResponse> {
this.allEpeople = [...this.allEpeople, ePerson]
return observableOf(new RestResponse(true, 200, 'Success'));
},
updateEPerson(ePerson: EPerson): Observable<RestResponse> {
this.allEpeople.forEach((ePersonInList: EPerson, i: number) => {
if (ePersonInList.id === ePerson.id) {
this.allEpeople[i] = ePerson;
}
});
return observableOf(new RestResponse(true, 200, 'Success'));
}
};
builderService = getMockFormBuilderService();
translateService = getMockTranslateService();
TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
}),
],
declarations: [EPeopleRegistryComponent, EPersonFormComponent],
providers: [EPersonFormComponent,
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: FormBuilderService, useValue: builderService },
EPeopleRegistryComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EPersonFormComponent);
component = fixture.componentInstance;
component.ngOnInit();
fixture.detectChanges();
});
it('should create EPersonFormComponent', inject([EPersonFormComponent], (comp: EPersonFormComponent) => {
expect(comp).toBeDefined();
}));
describe('when submitting the form', () => {
const firstName = 'testName';
const lastName = 'testLastName';
const email = 'testEmail@test.com';
const canLogIn = false;
const requireCertificate = false;
const expected = Object.assign(new EPerson(), {
metadata: {
'eperson.firstname': [
{
value: firstName
}
],
'eperson.lastname': [
{
value: lastName
},
],
},
email: email,
canLogIn: canLogIn,
requireCertificate: requireCertificate,
});
beforeEach(() => {
spyOn(component.submitForm, 'emit');
component.firstName.value = firstName;
component.lastName.value = lastName;
component.email.value = email;
component.canLogIn.value = canLogIn;
component.requireCertificate.value = requireCertificate;
});
describe('without active EPerson', () => {
beforeEach(() => {
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(undefined));
component.onSubmit();
fixture.detectChanges();
});
it('should emit a new eperson using the correct values', async(() => {
fixture.whenStable().then(() => {
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
});
}));
});
describe('with an active eperson', () => {
const expectedWithId = Object.assign(new EPerson(), {
metadata: {
'eperson.firstname': [
{
value: firstName
}
],
'eperson.lastname': [
{
value: lastName
},
],
},
email: email,
canLogIn: canLogIn,
requireCertificate: requireCertificate,
});
beforeEach(() => {
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId));
component.onSubmit();
fixture.detectChanges();
});
it('should emit the existing eperson using the correct values', async(() => {
fixture.whenStable().then(() => {
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
});
}));
});
});
});

View File

@@ -0,0 +1,344 @@
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
DynamicCheckboxModel,
DynamicFormControlModel,
DynamicFormLayout,
DynamicInputModel
} from '@ng-dynamic-forms/core';
import { TranslateService } from '@ngx-translate/core';
import { combineLatest } from 'rxjs/internal/observable/combineLatest';
import { Subscription } from 'rxjs/internal/Subscription';
import { take } from 'rxjs/operators';
import { RestResponse } from '../../../../core/cache/response.models';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
import { EPerson } from '../../../../core/eperson/models/eperson.model';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
import { hasValue } from '../../../../shared/empty.util';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
@Component({
selector: 'ds-eperson-form',
templateUrl: './eperson-form.component.html'
})
/**
* A form used for creating and editing EPeople
*/
export class EPersonFormComponent implements OnInit, OnDestroy {
labelPrefix = 'admin.access-control.epeople.form.';
/**
* A unique id used for ds-form
*/
formId = 'eperson-form';
/**
* The labelPrefix for all messages related to this form
*/
messagePrefix = 'admin.access-control.epeople.form';
/**
* Dynamic input models for the inputs of form
*/
firstName: DynamicInputModel;
lastName: DynamicInputModel;
email: DynamicInputModel;
// booleans
canLogIn: DynamicCheckboxModel;
requireCertificate: DynamicCheckboxModel;
/**
* A list of all dynamic input models
*/
formModel: DynamicFormControlModel[];
/**
* Layout used for structuring the form inputs
*/
formLayout: DynamicFormLayout = {
firstName: {
grid: {
host: 'row'
}
},
lastName: {
grid: {
host: 'row'
}
},
email: {
grid: {
host: 'row'
}
},
canLogIn: {
grid: {
host: 'col col-sm-6 d-inline-block'
}
},
requireCertificate: {
grid: {
host: 'col col-sm-6 d-inline-block'
}
},
};
/**
* A FormGroup that combines all inputs
*/
formGroup: FormGroup;
/**
* An EventEmitter that's fired whenever the form is being submitted
*/
@Output() submitForm: EventEmitter<any> = new EventEmitter();
/**
* An EventEmitter that's fired whenever the form is cancelled
*/
@Output() cancelForm: EventEmitter<any> = new EventEmitter();
/**
* List of subscriptions
*/
subs: Subscription[] = [];
/**
* Try to retrieve initial active eperson, to fill in checkboxes at component creation
*/
epersonInitial: EPerson;
constructor(public epersonService: EPersonDataService,
private formBuilderService: FormBuilderService,
private translateService: TranslateService,
private notificationsService: NotificationsService,) {
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
this.epersonInitial = eperson;
}));
}
ngOnInit() {
combineLatest(
this.translateService.get(`${this.messagePrefix}.firstName`),
this.translateService.get(`${this.messagePrefix}.lastName`),
this.translateService.get(`${this.messagePrefix}.email`),
this.translateService.get(`${this.messagePrefix}.canLogIn`),
this.translateService.get(`${this.messagePrefix}.requireCertificate`),
this.translateService.get(`${this.messagePrefix}.emailHint`),
).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => {
this.firstName = new DynamicInputModel({
id: 'firstName',
label: firstName,
name: 'firstName',
validators: {
required: null,
},
required: true,
});
this.lastName = new DynamicInputModel({
id: 'lastName',
label: lastName,
name: 'lastName',
validators: {
required: null,
},
required: true,
});
this.email = new DynamicInputModel({
id: 'email',
label: email,
name: 'email',
validators: {
required: null,
pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$'
},
required: true,
hint: emailHint
});
this.canLogIn = new DynamicCheckboxModel(
{
id: 'canLogIn',
label: canLogIn,
name: 'canLogIn',
value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true)
});
this.requireCertificate = new DynamicCheckboxModel(
{
id: 'requireCertificate',
label: requireCertificate,
name: 'requireCertificate',
value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false)
});
this.formModel = [
this.firstName,
this.lastName,
this.email,
this.canLogIn,
this.requireCertificate,
];
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
this.formGroup.patchValue({
firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '',
lastName: eperson != null ? eperson.firstMetadataValue('eperson.lastname') : '',
email: eperson != null ? eperson.email : '',
canLogIn: eperson != null ? eperson.canLogIn : true,
requireCertificate: eperson != null ? eperson.requireCertificate : false
});
}));
});
}
/**
* Stop editing the currently selected eperson
*/
onCancel() {
this.epersonService.cancelEditEPerson();
this.cancelForm.emit();
}
/**
* Submit the form
* When the eperson has an id attached -> Edit the eperson
* When the eperson has no id attached -> Create new eperson
* Emit the updated/created eperson using the EventEmitter submitForm
*/
onSubmit() {
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe(
(ePerson: EPerson) => {
console.log('onsubmit ep', ePerson)
const values = {
metadata: {
'eperson.firstname': [
{
value: this.firstName.value
}
],
'eperson.lastname': [
{
value: this.lastName.value
},
],
},
email: this.email.value,
canLogIn: this.canLogIn.value,
requireCertificate: this.requireCertificate.value,
};
if (ePerson == null) {
this.createNewEPerson(values);
} else {
this.editEPerson(ePerson, values);
}
}
);
}
/**
* Creates new EPerson based on given values from form
* @param values
*/
createNewEPerson(values) {
console.log('createNewEPerson(values)', values)
const ePersonToCreate = Object.assign(new EPerson(), values);
const response = this.epersonService.tryToCreate(ePersonToCreate);
response.pipe(take(1)).subscribe((restResponse: RestResponse) => {
if (restResponse.isSuccessful) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: ePersonToCreate.name }));
this.submitForm.emit(ePersonToCreate);
} else {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: ePersonToCreate.name }));
this.cancelForm.emit();
}
});
this.showNotificationIfEmailInUse(ePersonToCreate, 'created');
}
/**
* Edits existing EPerson based on given values from form and old EPerson
* @param ePerson ePerson to edit
* @param values new ePerson values (of form)
*/
editEPerson(ePerson: EPerson, values) {
const editedEperson = Object.assign(new EPerson(), {
id: ePerson.id,
metadata: {
'eperson.firstname': [
{
value: (this.firstName.value ? this.firstName.value : ePerson.firstMetadataValue('eperson.firstname'))
}
],
'eperson.lastname': [
{
value: (this.lastName.value ? this.lastName.value : ePerson.firstMetadataValue('eperson.lastname'))
},
],
},
email: (hasValue(values.email) ? values.email : ePerson.email),
canLogIn: (hasValue(values.canLogIn) ? values.canLogIn : ePerson.canLogIn),
requireCertificate: (hasValue(values.requireCertificate) ? values.requireCertificate : ePerson.requireCertificate),
_links: ePerson._links,
});
const response = this.epersonService.updateEPerson(editedEperson);
response.pipe(take(1)).subscribe((restResponse: RestResponse) => {
if (restResponse.isSuccessful) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: editedEperson.name }));
this.submitForm.emit(editedEperson);
} else {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: editedEperson.name }));
this.cancelForm.emit();
}
});
if (values.email != null && values.email !== ePerson.email) {
this.showNotificationIfEmailInUse(editedEperson, 'edited');
}
}
/**
* Checks for the given ePerson if there is already an ePerson in the system with that email
* and shows notification if this is the case
* @param ePerson ePerson values to check
* @param notificationSection whether in create or edit
*/
private showNotificationIfEmailInUse(ePerson: EPerson, notificationSection: string) {
// Relevant message for email in use
this.subs.push(this.epersonService.searchByScope('email', ePerson.email, {
currentPage: 1,
elementsPerPage: 0
}).pipe(getSucceededRemoteData(), getRemoteDataPayload())
.subscribe((list: PaginatedList<EPerson>) => {
if (list.totalElements > 0) {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.' + notificationSection + '.failure.emailInUse', {
name: ePerson.name,
email: ePerson.email
}));
}
}));
}
/**
* Reset all input-fields to be empty
*/
clearFields() {
this.formGroup.patchValue({
firstName: '',
lastName: '',
email: '',
canLogin: true,
requireCertificate: false
});
}
/**
* Cancel the current edit when component is destroyed & unsub all subscriptions
*/
ngOnDestroy(): void {
this.onCancel();
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -230,10 +230,10 @@ describe('BitstreamFormatsComponent', () => {
comp.deleteFormats();
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat1);
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat2);
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat3);
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat4);
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat1.id);
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat2.id);
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat3.id);
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat4.id);
expect(notificationsServiceStub.success).toHaveBeenCalledWith('admin.registries.bitstream-formats.delete.success.head',
'admin.registries.bitstream-formats.delete.success.amount');
@@ -276,10 +276,10 @@ describe('BitstreamFormatsComponent', () => {
comp.deleteFormats();
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat1);
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat2);
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat3);
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat4);
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat1.id);
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat2.id);
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat3.id);
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat4.id);
expect(notificationsServiceStub.error).toHaveBeenCalledWith('admin.registries.bitstream-formats.delete.failure.head',
'admin.registries.bitstream-formats.delete.failure.amount');

View File

@@ -64,7 +64,7 @@ export class BitstreamFormatsComponent implements OnInit {
const tasks$ = [];
for (const format of formats) {
if (hasValue(format.id)) {
tasks$.push(this.bitstreamFormatService.delete(format));
tasks$.push(this.bitstreamFormatService.delete(format.id));
}
}
zip(...tasks$).subscribe((results: boolean[]) => {

View File

@@ -2,8 +2,11 @@ import { RouterModule } from '@angular/router';
import { NgModule } from '@angular/core';
import { URLCombiner } from '../core/url-combiner/url-combiner';
import { getAdminModulePath } from '../app-routing.module';
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
const REGISTRIES_MODULE_PATH = 'registries';
const ACCESS_CONTROL_MODULE_PATH = 'access-control';
export function getRegistriesModulePath() {
return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString();
@@ -15,7 +18,17 @@ export function getRegistriesModulePath() {
{
path: REGISTRIES_MODULE_PATH,
loadChildren: './admin-registries/admin-registries.module#AdminRegistriesModule'
}
},
{
path: ACCESS_CONTROL_MODULE_PATH,
loadChildren: './admin-access-control/admin-access-control.module#AdminAccessControlModule'
},
{
path: 'search',
resolve: { breadcrumb: I18nBreadcrumbResolver },
component: AdminSearchPageComponent,
data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }
},
])
]
})

View File

@@ -0,0 +1 @@
<ds-configuration-search-page configuration="administrativeView" [context]="context"></ds-configuration-search-page>

View File

@@ -0,0 +1,27 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminSearchPageComponent } from './admin-search-page.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('AdminSearchPageComponent', () => {
let component: AdminSearchPageComponent;
let fixture: ComponentFixture<AdminSearchPageComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AdminSearchPageComponent ],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AdminSearchPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,18 @@
import { Component, OnInit } from '@angular/core';
import { Context } from '../../core/shared/context.model';
@Component({
selector: 'ds-admin-search-page',
templateUrl: './admin-search-page.component.html',
styleUrls: ['./admin-search-page.component.scss']
})
/**
* Component that represents a search page for administrators
*/
export class AdminSearchPageComponent {
/**
* The context of this page
*/
context: Context = Context.AdminSearch;
}

View File

@@ -0,0 +1,13 @@
<ds-collection-search-result-grid-element [object]="object"
[index]="index"
[linkType]="linkType"
[listID]="listID">
<ul class="list-group list-group-flush">
<li class="list-group-item text-center">
<a class="btn btn-light btn-sm btn-auto my-1 edit-link" [routerLink]="[editPath]">
<i class="fa fa-edit"></i>
</a>
</li>
</ul>
</ds-collection-search-result-grid-element>

View File

@@ -0,0 +1,66 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service';
import { SharedModule } from '../../../../../shared/shared.module';
import { CollectionAdminSearchResultGridElementComponent } from './collection-admin-search-result-grid-element.component';
import { TranslateModule } from '@ngx-translate/core';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model';
import { Collection } from '../../../../../core/shared/collection.model';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { getCollectionEditPath } from '../../../../../+collection-page/collection-page-routing.module';
describe('CollectionAdminSearchResultGridElementComponent', () => {
let component: CollectionAdminSearchResultGridElementComponent;
let fixture: ComponentFixture<CollectionAdminSearchResultGridElementComponent>;
let id;
let searchResult;
function init() {
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
searchResult = new CollectionSearchResult();
searchResult.indexableObject = new Collection();
searchResult.indexableObject.uuid = id;
}
beforeEach(async(() => {
init();
TestBed.configureTestingModule({
imports: [
NoopAnimationsModule,
TranslateModule.forRoot(),
RouterTestingModule.withRoutes([]),
SharedModule
],
declarations: [CollectionAdminSearchResultGridElementComponent],
providers: [
{ provide: TruncatableService, useValue: mockTruncatableService },
{ provide: BitstreamDataService, useValue: {} },
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CollectionAdminSearchResultGridElementComponent);
component = fixture.componentInstance;
component.object = searchResult;
component.linkTypes = CollectionElementLinkType;
component.index = 0;
component.viewModes = ViewMode;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render an edit button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.edit-link'));
const link = a.nativeElement.href;
expect(link).toContain(getCollectionEditPath(id));
})
});

View File

@@ -0,0 +1,26 @@
import { Component } from '@angular/core';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { Context } from '../../../../../core/shared/context.model';
import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model';
import { Collection } from '../../../../../core/shared/collection.model';
import { getCollectionEditPath } from '../../../../../+collection-page/collection-page-routing.module';
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
@listableObjectComponent(CollectionSearchResult, ViewMode.GridElement, Context.AdminSearch)
@Component({
selector: 'ds-collection-admin-search-result-list-element',
styleUrls: ['./collection-admin-search-result-grid-element.component.scss'],
templateUrl: './collection-admin-search-result-grid-element.component.html'
})
/**
* The component for displaying a list element for a collection search result on the admin search page
*/
export class CollectionAdminSearchResultGridElementComponent extends SearchResultGridElementComponent<CollectionSearchResult, Collection> {
editPath: string;
ngOnInit() {
super.ngOnInit();
this.editPath = getCollectionEditPath(this.dso.uuid);
}
}

View File

@@ -0,0 +1,13 @@
<ds-community-search-result-grid-element [object]="object"
[index]="index"
[linkType]="linkType"
[listID]="listID">
<ul class="list-group list-group-flush">
<li class="list-group-item text-center">
<a class="btn btn-light btn-sm btn-auto my-1 edit-link" [routerLink]="[editPath]">
<i class="fa fa-edit"></i>
</a>
</li>
</ul>
</ds-community-search-result-grid-element>

View File

@@ -0,0 +1,70 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core';
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service';
import { SharedModule } from '../../../../../shared/shared.module';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { CommunityAdminSearchResultGridElementComponent } from './community-admin-search-result-grid-element.component';
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
import { getCommunityEditPath } from '../../../../../+community-page/community-page-routing.module';
import { Community } from '../../../../../core/shared/community.model';
import { CommunityAdminSearchResultListElementComponent } from '../../admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component';
describe('CommunityAdminSearchResultGridElementComponent', () => {
let component: CommunityAdminSearchResultGridElementComponent;
let fixture: ComponentFixture<CommunityAdminSearchResultGridElementComponent>;
let id;
let searchResult;
function init() {
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
searchResult = new CommunitySearchResult();
searchResult.indexableObject = new Community();
searchResult.indexableObject.uuid = id;
}
beforeEach(async(() => {
init();
TestBed.configureTestingModule({
imports: [
NoopAnimationsModule,
TranslateModule.forRoot(),
RouterTestingModule.withRoutes([]),
SharedModule
],
declarations: [CommunityAdminSearchResultGridElementComponent],
providers: [
{ provide: TruncatableService, useValue: mockTruncatableService },
{ provide: BitstreamDataService, useValue: {} },
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CommunityAdminSearchResultGridElementComponent);
component = fixture.componentInstance;
component.object = searchResult;
component.linkTypes = CollectionElementLinkType;
component.index = 0;
component.viewModes = ViewMode;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render an edit button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.edit-link'));
const link = a.nativeElement.href;
expect(link).toContain(getCommunityEditPath(id));
})
});

View File

@@ -0,0 +1,26 @@
import { Component } from '@angular/core';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { Context } from '../../../../../core/shared/context.model';
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
import { Community } from '../../../../../core/shared/community.model';
import { getCommunityEditPath } from '../../../../../+community-page/community-page-routing.module';
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
@listableObjectComponent(CommunitySearchResult, ViewMode.GridElement, Context.AdminSearch)
@Component({
selector: 'ds-community-admin-search-result-grid-element',
styleUrls: ['./community-admin-search-result-grid-element.component.scss'],
templateUrl: './community-admin-search-result-grid-element.component.html'
})
/**
* The component for displaying a list element for a community search result on the admin search page
*/
export class CommunityAdminSearchResultGridElementComponent extends SearchResultGridElementComponent<CommunitySearchResult, Community> {
editPath: string;
ngOnInit() {
super.ngOnInit();
this.editPath = getCommunityEditPath(this.dso.uuid);
}
}

View File

@@ -0,0 +1,15 @@
<ng-template dsListableObject>
</ng-template>
<div #badges class="position-absolute ml-1">
<div *ngIf="dso && !dso.isDiscoverable" class="private-badge">
<span class="badge badge-danger">{{ "admin.search.item.private" | translate }}</span>
</div>
<div *ngIf="dso && dso.isWithdrawn" class="withdrawn-badge">
<span class="badge badge-warning">{{ "admin.search.item.withdrawn" | translate }}</span>
</div>
</div>
<ul #buttons class="list-group list-group-flush">
<li class="list-group-item">
<ds-item-admin-search-result-actions-element class="d-flex justify-content-between" [item]="dso" [small]="true"></ds-item-admin-search-result-actions-element>
</li>
</ul>

View File

@@ -0,0 +1,121 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs/internal/Observable';
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
import { RemoteData } from '../../../../../core/data/remote-data';
import { Bitstream } from '../../../../../core/shared/bitstream.model';
import { Item } from '../../../../../core/shared/item.model';
import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service';
import { SharedModule } from '../../../../../shared/shared.module';
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-result-grid-element.component';
describe('ItemAdminSearchResultGridElementComponent', () => {
let component: ItemAdminSearchResultGridElementComponent;
let fixture: ComponentFixture<ItemAdminSearchResultGridElementComponent>;
let id;
let searchResult;
const mockBitstreamDataService = {
getThumbnailFor(item: Item): Observable<RemoteData<Bitstream>> {
return createSuccessfulRemoteDataObject$(new Bitstream());
}
};
function init() {
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
searchResult = new ItemSearchResult();
searchResult.indexableObject = new Item();
searchResult.indexableObject.uuid = id;
}
beforeEach(async(() => {
init();
TestBed.configureTestingModule(
{
declarations: [ItemAdminSearchResultGridElementComponent],
imports: [
NoopAnimationsModule,
TranslateModule.forRoot(),
RouterTestingModule.withRoutes([]),
SharedModule
],
providers: [
{ provide: TruncatableService, useValue: mockTruncatableService },
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ItemAdminSearchResultGridElementComponent);
component = fixture.componentInstance;
component.object = searchResult;
component.linkTypes = CollectionElementLinkType;
component.index = 0;
component.viewModes = ViewMode;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('when the item is not withdrawn', () => {
beforeEach(() => {
component.dso.isWithdrawn = false;
fixture.detectChanges();
});
it('should not show the withdrawn badge', () => {
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
expect(badge).toBeNull();
});
});
describe('when the item is withdrawn', () => {
beforeEach(() => {
component.dso.isWithdrawn = true;
fixture.detectChanges();
});
it('should show the withdrawn badge', () => {
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
expect(badge).not.toBeNull();
});
});
describe('when the item is not private', () => {
beforeEach(() => {
component.dso.isDiscoverable = true;
fixture.detectChanges();
});
it('should not show the private badge', () => {
const badge = fixture.debugElement.query(By.css('div.private-badge'));
expect(badge).toBeNull();
});
});
describe('when the item is private', () => {
beforeEach(() => {
component.dso.isDiscoverable = false;
fixture.detectChanges();
});
it('should show the private badge', () => {
const badge = fixture.debugElement.query(By.css('div.private-badge'));
expect(badge).not.toBeNull();
});
})
});

View File

@@ -0,0 +1,75 @@
import { Component, ComponentFactoryResolver, ElementRef, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { Item } from '../../../../../core/shared/item.model';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { getListableObjectComponent, listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { Context } from '../../../../../core/shared/context.model';
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
import { getItemEditPath } from '../../../../../+item-page/item-page-routing.module';
import { URLCombiner } from '../../../../../core/url-combiner/url-combiner';
import {
ITEM_EDIT_DELETE_PATH,
ITEM_EDIT_MOVE_PATH,
ITEM_EDIT_PRIVATE_PATH,
ITEM_EDIT_PUBLIC_PATH,
ITEM_EDIT_REINSTATE_PATH,
ITEM_EDIT_WITHDRAW_PATH
} from '../../../../../+item-page/edit-item-page/edit-item-page.routing.module';
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
@listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminSearch)
@Component({
selector: 'ds-item-admin-search-result-grid-element',
styleUrls: ['./item-admin-search-result-grid-element.component.scss'],
templateUrl: './item-admin-search-result-grid-element.component.html'
})
/**
* The component for displaying a list element for an item search result on the admin search page
*/
export class ItemAdminSearchResultGridElementComponent extends SearchResultGridElementComponent<ItemSearchResult, Item> implements OnInit {
@ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective;
@ViewChild('badges', { static: true }) badges: ElementRef;
@ViewChild('buttons', { static: true }) buttons: ElementRef;
constructor(protected truncatableService: TruncatableService,
protected bitstreamDataService: BitstreamDataService,
private componentFactoryResolver: ComponentFactoryResolver
) {
super(truncatableService, bitstreamDataService);
}
/**
* Setup the dynamic child component
*/
ngOnInit(): void {
super.ngOnInit();
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent());
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(
componentFactory,
0,
undefined,
[
[this.badges.nativeElement],
[this.buttons.nativeElement]
]);
(componentRef.instance as any).object = this.object;
(componentRef.instance as any).index = this.index;
(componentRef.instance as any).linkType = this.linkType;
(componentRef.instance as any).listID = this.listID;
}
/**
* Fetch the component depending on the item's relationship type, view mode and context
* @returns {GenericConstructor<Component>}
*/
private getComponent(): GenericConstructor<Component> {
return getListableObjectComponent(this.object.getRenderTypes(), ViewMode.GridElement, undefined)
}
}

View File

@@ -0,0 +1,9 @@
<ds-collection-search-result-list-element [object]="object"
[index]="index"
[linkType]="linkType"
[listID]="listID"></ds-collection-search-result-list-element>
<div>
<a class="btn btn-light mt-1" [routerLink]="[editPath]">
<i class="fa fa-edit"></i> {{"admin.search.collection.edit" | translate}}
</a>
</div>

View File

@@ -0,0 +1,60 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { CollectionAdminSearchResultListElementComponent } from './collection-admin-search-result-list-element.component';
import { TranslateModule } from '@ngx-translate/core';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model';
import { Collection } from '../../../../../core/shared/collection.model';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { getCollectionEditPath } from '../../../../../+collection-page/collection-page-routing.module';
describe('CollectionAdminSearchResultListElementComponent', () => {
let component: CollectionAdminSearchResultListElementComponent;
let fixture: ComponentFixture<CollectionAdminSearchResultListElementComponent>;
let id;
let searchResult;
function init() {
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
searchResult = new CollectionSearchResult();
searchResult.indexableObject = new Collection();
searchResult.indexableObject.uuid = id;
}
beforeEach(async(() => {
init();
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
RouterTestingModule.withRoutes([])
],
declarations: [CollectionAdminSearchResultListElementComponent],
providers: [{ provide: TruncatableService, useValue: {} }],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CollectionAdminSearchResultListElementComponent);
component = fixture.componentInstance;
component.object = searchResult;
component.linkTypes = CollectionElementLinkType;
component.index = 0;
component.viewModes = ViewMode;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render an edit button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a'));
const link = a.nativeElement.href;
expect(link).toContain(getCollectionEditPath(id));
})
});

View File

@@ -0,0 +1,26 @@
import { Component } from '@angular/core';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { Context } from '../../../../../core/shared/context.model';
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model';
import { Collection } from '../../../../../core/shared/collection.model';
import { getCollectionEditPath } from '../../../../../+collection-page/collection-page-routing.module';
@listableObjectComponent(CollectionSearchResult, ViewMode.ListElement, Context.AdminSearch)
@Component({
selector: 'ds-collection-admin-search-result-list-element',
styleUrls: ['./collection-admin-search-result-list-element.component.scss'],
templateUrl: './collection-admin-search-result-list-element.component.html'
})
/**
* The component for displaying a list element for a collection search result on the admin search page
*/
export class CollectionAdminSearchResultListElementComponent extends SearchResultListElementComponent<CollectionSearchResult, Collection> {
editPath: string;
ngOnInit() {
super.ngOnInit();
this.editPath = getCollectionEditPath(this.dso.uuid);
}
}

View File

@@ -0,0 +1,9 @@
<ds-community-search-result-list-element [object]="object"
[index]="index"
[linkType]="linkType"
[listID]="listID"></ds-community-search-result-list-element>
<div>
<a class="btn btn-light mt-1" [routerLink]="[editPath]">
<i class="fa fa-edit"></i> {{"admin.search.community.edit" | translate}}
</a>
</div>

View File

@@ -0,0 +1,60 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { CommunityAdminSearchResultListElementComponent } from './community-admin-search-result-list-element.component';
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
import { getCommunityEditPath } from '../../../../../+community-page/community-page-routing.module';
import { Community } from '../../../../../core/shared/community.model';
describe('CommunityAdminSearchResultListElementComponent', () => {
let component: CommunityAdminSearchResultListElementComponent;
let fixture: ComponentFixture<CommunityAdminSearchResultListElementComponent>;
let id;
let searchResult;
function init() {
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
searchResult = new CommunitySearchResult();
searchResult.indexableObject = new Community();
searchResult.indexableObject.uuid = id;
}
beforeEach(async(() => {
init();
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
RouterTestingModule.withRoutes([])
],
declarations: [CommunityAdminSearchResultListElementComponent],
providers: [{ provide: TruncatableService, useValue: {} }],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CommunityAdminSearchResultListElementComponent);
component = fixture.componentInstance;
component.object = searchResult;
component.linkTypes = CollectionElementLinkType;
component.index = 0;
component.viewModes = ViewMode;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render an edit button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a'));
const link = a.nativeElement.href;
expect(link).toContain(getCommunityEditPath(id));
})
});

View File

@@ -0,0 +1,26 @@
import { Component } from '@angular/core';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { Context } from '../../../../../core/shared/context.model';
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
import { Community } from '../../../../../core/shared/community.model';
import { getCommunityEditPath } from '../../../../../+community-page/community-page-routing.module';
@listableObjectComponent(CommunitySearchResult, ViewMode.ListElement, Context.AdminSearch)
@Component({
selector: 'ds-community-admin-search-result-list-element',
styleUrls: ['./community-admin-search-result-list-element.component.scss'],
templateUrl: './community-admin-search-result-list-element.component.html'
})
/**
* The component for displaying a list element for a community search result on the admin search page
*/
export class CommunityAdminSearchResultListElementComponent extends SearchResultListElementComponent<CommunitySearchResult, Community> {
editPath: string;
ngOnInit() {
super.ngOnInit();
this.editPath = getCommunityEditPath(this.dso.uuid);
}
}

View File

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

View File

@@ -0,0 +1,101 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
import { ItemAdminSearchResultListElementComponent } from './item-admin-search-result-list-element.component';
import { Item } from '../../../../../core/shared/item.model';
describe('ItemAdminSearchResultListElementComponent', () => {
let component: ItemAdminSearchResultListElementComponent;
let fixture: ComponentFixture<ItemAdminSearchResultListElementComponent>;
let id;
let searchResult;
function init() {
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
searchResult = new ItemSearchResult();
searchResult.indexableObject = new Item();
searchResult.indexableObject.uuid = id;
}
beforeEach(async(() => {
init();
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
RouterTestingModule.withRoutes([])
],
declarations: [ItemAdminSearchResultListElementComponent],
providers: [{ provide: TruncatableService, useValue: {} }],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ItemAdminSearchResultListElementComponent);
component = fixture.componentInstance;
component.object = searchResult;
component.linkTypes = CollectionElementLinkType;
component.index = 0;
component.viewModes = ViewMode;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('when the item is not withdrawn', () => {
beforeEach(() => {
component.dso.isWithdrawn = false;
fixture.detectChanges();
});
it('should not show the withdrawn badge', () => {
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
expect(badge).toBeNull();
});
});
describe('when the item is withdrawn', () => {
beforeEach(() => {
component.dso.isWithdrawn = true;
fixture.detectChanges();
});
it('should show the withdrawn badge', () => {
const badge = fixture.debugElement.query(By.css('div.withdrawn-badge'));
expect(badge).not.toBeNull();
});
});
describe('when the item is not private', () => {
beforeEach(() => {
component.dso.isDiscoverable = true;
fixture.detectChanges();
});
it('should not show the private badge', () => {
const badge = fixture.debugElement.query(By.css('div.private-badge'));
expect(badge).toBeNull();
});
});
describe('when the item is private', () => {
beforeEach(() => {
component.dso.isDiscoverable = false;
fixture.detectChanges();
});
it('should show the private badge', () => {
const badge = fixture.debugElement.query(By.css('div.private-badge'));
expect(badge).not.toBeNull();
});
})
});

View File

@@ -0,0 +1,20 @@
import { Component } from '@angular/core';
import { Item } from '../../../../../core/shared/item.model';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { Context } from '../../../../../core/shared/context.model';
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
@listableObjectComponent(ItemSearchResult, ViewMode.ListElement, Context.AdminSearch)
@Component({
selector: 'ds-item-admin-search-result-list-element',
styleUrls: ['./item-admin-search-result-list-element.component.scss'],
templateUrl: './item-admin-search-result-list-element.component.html'
})
/**
* The component for displaying a list element for an item search result on the admin search page
*/
export class ItemAdminSearchResultListElementComponent extends SearchResultListElementComponent<ItemSearchResult, Item> {
}

View File

@@ -0,0 +1,27 @@
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 edit-link" [routerLink]="[getEditPath()]" [title]="'admin.search.item.edit' | translate">
<i class="fa fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span>
</a>
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isWithdrawn" class="btn btn-light my-1 withdraw-link" [routerLink]="[getWithdrawPath()]" [title]="'admin.search.item.withdraw' | translate">
<i class="fa fa-ban"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span>
</a>
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isWithdrawn" class="btn btn-light my-1 reinstate-link" [routerLink]="[getReinstatePath()]" [title]="'admin.search.item.reinstate' | translate">
<i class="fa fa-undo"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.reinstate" | translate}}</span>
</a>
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isDiscoverable" class="btn btn-light my-1 private-link" [routerLink]="[getPrivatePath()]" [title]="'admin.search.item.make-private' | translate">
<i class="fa fa-eye-slash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-private" | translate}}</span>
</a>
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isDiscoverable" class="btn btn-light my-1 public-link" [routerLink]="[getPublicPath()]" [title]="'admin.search.item.make-public' | translate">
<i class="fa fa-eye"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-public" | translate}}</span>
</a>
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 delete-link" [routerLink]="[getDeletePath()]" [title]="'admin.search.item.delete' | translate">
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.delete" | translate}}</span>
</a>
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 move-link" [routerLink]="[getMovePath()]" [title]="'admin.search.item.move' | translate">
<i class="fa fa-arrow-circle-right"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span>
</a>

View File

@@ -0,0 +1,144 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { ItemAdminSearchResultActionsComponent } from './item-admin-search-result-actions.component';
import { Item } from '../../../core/shared/item.model';
import {
ITEM_EDIT_DELETE_PATH,
ITEM_EDIT_MOVE_PATH,
ITEM_EDIT_PRIVATE_PATH,
ITEM_EDIT_PUBLIC_PATH,
ITEM_EDIT_REINSTATE_PATH,
ITEM_EDIT_WITHDRAW_PATH
} from '../../../+item-page/edit-item-page/edit-item-page.routing.module';
import { getItemEditPath } from '../../../+item-page/item-page-routing.module';
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
describe('ItemAdminSearchResultActionsComponent', () => {
let component: ItemAdminSearchResultActionsComponent;
let fixture: ComponentFixture<ItemAdminSearchResultActionsComponent>;
let id;
let item;
function init() {
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
item = new Item();
item.uuid = id;
}
beforeEach(async(() => {
init();
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
RouterTestingModule.withRoutes([])
],
declarations: [ItemAdminSearchResultActionsComponent],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ItemAdminSearchResultActionsComponent);
component = fixture.componentInstance;
component.item = item;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render an edit button with the correct link', () => {
const button = fixture.debugElement.query(By.css('a.edit-link'));
const link = button.nativeElement.href;
expect(link).toContain(getItemEditPath(id));
});
it('should render a delete button with the correct link', () => {
const button = fixture.debugElement.query(By.css('a.delete-link'));
const link = button.nativeElement.href;
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_DELETE_PATH).toString());
});
it('should render a move button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.move-link'));
const link = a.nativeElement.href;
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_MOVE_PATH).toString());
});
describe('when the item is not withdrawn', () => {
beforeEach(() => {
component.item.isWithdrawn = false;
fixture.detectChanges();
});
it('should render a withdraw button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.withdraw-link'));
const link = a.nativeElement.href;
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_WITHDRAW_PATH).toString());
});
it('should not render a reinstate button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.reinstate-link'));
expect(a).toBeNull();
});
});
describe('when the item is withdrawn', () => {
beforeEach(() => {
component.item.isWithdrawn = true;
fixture.detectChanges();
});
it('should not render a withdraw button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.withdraw-link'));
expect(a).toBeNull();
});
it('should render a reinstate button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.reinstate-link'));
const link = a.nativeElement.href;
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_REINSTATE_PATH).toString());
});
});
describe('when the item is not private', () => {
beforeEach(() => {
component.item.isDiscoverable = true;
fixture.detectChanges();
});
it('should render a make private button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.private-link'));
const link = a.nativeElement.href;
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_PRIVATE_PATH).toString());
});
it('should not render a make public button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.public-link'));
expect(a).toBeNull();
});
});
describe('when the item is private', () => {
beforeEach(() => {
component.item.isDiscoverable = false;
fixture.detectChanges();
});
it('should not render a make private button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.private-link'));
expect(a).toBeNull();
});
it('should render a make private button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.public-link'));
const link = a.nativeElement.href;
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_PUBLIC_PATH).toString());
});
})
});

View File

@@ -0,0 +1,81 @@
import { Component, Input } from '@angular/core';
import { Item } from '../../../core/shared/item.model';
import { getItemEditPath } from '../../../+item-page/item-page-routing.module';
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
import {
ITEM_EDIT_DELETE_PATH,
ITEM_EDIT_MOVE_PATH,
ITEM_EDIT_PRIVATE_PATH,
ITEM_EDIT_PUBLIC_PATH,
ITEM_EDIT_REINSTATE_PATH,
ITEM_EDIT_WITHDRAW_PATH
} from '../../../+item-page/edit-item-page/edit-item-page.routing.module';
@Component({
selector: 'ds-item-admin-search-result-actions-element',
styleUrls: ['./item-admin-search-result-actions.component.scss'],
templateUrl: './item-admin-search-result-actions.component.html'
})
/**
* The component for displaying the actions for a list element for an item search result on the admin search page
*/
export class ItemAdminSearchResultActionsComponent {
/**
* The item to perform the actions on
*/
@Input() public item: Item;
/**
* Whether or not to use small buttons
*/
@Input() public small: boolean;
/**
* Returns the path to the edit page of this item
*/
getEditPath(): string {
return getItemEditPath(this.item.uuid)
}
/**
* Returns the path to the move page of this item
*/
getMovePath(): string {
return new URLCombiner(this.getEditPath(), ITEM_EDIT_MOVE_PATH).toString();
}
/**
* Returns the path to the delete page of this item
*/
getDeletePath(): string {
return new URLCombiner(this.getEditPath(), ITEM_EDIT_DELETE_PATH).toString();
}
/**
* Returns the path to the withdraw page of this item
*/
getWithdrawPath(): string {
return new URLCombiner(this.getEditPath(), ITEM_EDIT_WITHDRAW_PATH).toString();
}
/**
* Returns the path to the reinstate page of this item
*/
getReinstatePath(): string {
return new URLCombiner(this.getEditPath(), ITEM_EDIT_REINSTATE_PATH).toString();
}
/**
* Returns the path to the page where the user can make this item private
*/
getPrivatePath(): string {
return new URLCombiner(this.getEditPath(), ITEM_EDIT_PRIVATE_PATH).toString();
}
/**
* Returns the path to the page where the user can make this item public
*/
getPublicPath(): string {
return new URLCombiner(this.getEditPath(), ITEM_EDIT_PUBLIC_PATH).toString();
}
}

View File

@@ -1,23 +1,23 @@
import { Component, Injector, OnInit } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
import { MenuService } from '../../shared/menu/menu.service';
import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state';
import { MenuComponent } from '../../shared/menu/menu.component';
import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model';
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
import { AuthService } from '../../core/auth/auth.service';
import { first, map } from 'rxjs/operators';
import { combineLatest as combineLatestObservable } from 'rxjs';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
import { combineLatest as combineLatestObservable } from 'rxjs';
import { Observable } from 'rxjs/internal/Observable';
import { first, map } from 'rxjs/operators';
import { AuthService } from '../../core/auth/auth.service';
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state';
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model';
import { MenuComponent } from '../../shared/menu/menu.component';
import { MenuService } from '../../shared/menu/menu.service';
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
/**
* Component representing the admin sidebar
@@ -325,7 +325,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
model: {
type: MenuItemType.LINK,
text: 'menu.section.access_control_people',
link: ''
link: '/admin/access-control/epeople'
} as LinkMenuItemModel,
},
{
@@ -350,53 +350,19 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
link: ''
} as LinkMenuItemModel,
},
/* Search */
/* Admin Search */
{
id: 'find',
id: 'admin_search',
active: false,
visible: true,
model: {
type: MenuItemType.TEXT,
text: 'menu.section.find'
} as TextMenuItemModel,
type: MenuItemType.LINK,
text: 'menu.section.admin_search',
link: '/admin/search'
} as LinkMenuItemModel,
icon: 'search',
index: 5
},
{
id: 'find_items',
parentID: 'find',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.find_items',
link: '/search'
} as LinkMenuItemModel,
},
{
id: 'find_withdrawn_items',
parentID: 'find',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.find_withdrawn_items',
link: ''
} as LinkMenuItemModel,
},
{
id: 'find_private_items',
parentID: 'find',
active: false,
visible: true,
model: {
type: MenuItemType.LINK,
text: 'menu.section.find_private_items',
link: ''
} as LinkMenuItemModel,
},
/* Registries */
{
id: 'registries',

View File

@@ -1,14 +1,45 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { AdminAccessControlModule } from './admin-access-control/admin-access-control.module';
import { AdminRegistriesModule } from './admin-registries/admin-registries.module';
import { AdminRoutingModule } from './admin-routing.module';
import { SharedModule } from '../shared/shared.module';
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
import { SearchPageModule } from '../+search-page/search-page.module';
import { ItemAdminSearchResultListElementComponent } from './admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component';
import { CommunityAdminSearchResultListElementComponent } from './admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component';
import { CollectionAdminSearchResultListElementComponent } from './admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component';
import { ItemAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component';
import { CommunityAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component';
import { CollectionAdminSearchResultGridElementComponent } from './admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component';
import { ItemAdminSearchResultActionsComponent } from './admin-search-page/admin-search-results/item-admin-search-result-actions.component';
@NgModule({
imports: [
AdminRegistriesModule,
AdminRoutingModule,
AdminRegistriesModule,
AdminAccessControlModule,
SharedModule,
SearchPageModule
],
declarations: [
AdminSearchPageComponent,
ItemAdminSearchResultListElementComponent,
CommunityAdminSearchResultListElementComponent,
CollectionAdminSearchResultListElementComponent,
ItemAdminSearchResultGridElementComponent,
CommunityAdminSearchResultGridElementComponent,
CollectionAdminSearchResultGridElementComponent,
ItemAdminSearchResultActionsComponent
],
entryComponents: [
ItemAdminSearchResultListElementComponent,
CommunityAdminSearchResultListElementComponent,
CollectionAdminSearchResultListElementComponent,
ItemAdminSearchResultGridElementComponent,
CommunityAdminSearchResultGridElementComponent,
CollectionAdminSearchResultGridElementComponent,
ItemAdminSearchResultActionsComponent
]
})
export class AdminModule {

View File

@@ -20,7 +20,6 @@ import { Item } from '../../core/shared/item.model';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { Community } from '../../core/shared/community.model';
import { MockRouter } from '../../shared/mocks/mock-router';
import { ResourceType } from '../../core/shared/resource-type';
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
import { BrowseEntry } from '../../core/shared/browse-entry.model';
import { VarDirective } from '../../shared/utils/var.directive';

View File

@@ -0,0 +1,41 @@
import { Injectable } from '@angular/core';
import { Community } from '../core/shared/community.model';
import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service';
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
import { Collection } from '../core/shared/collection.model';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model';
import { Observable } from 'rxjs';
import { getRemoteDataPayload, getSucceededRemoteData } from '../core/shared/operators';
import { map } from 'rxjs/operators';
import { hasValue } from '../shared/empty.util';
import { getDSOPath } from '../app-routing.module';
/**
* The class that resolves the BreadcrumbConfig object for a DSpaceObject on a browse by page
*/
@Injectable()
export class BrowseByDSOBreadcrumbResolver {
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DSpaceObjectDataService) {
}
/**
* Method for resolving a breadcrumb config object
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns BreadcrumbConfig object
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<Community | Collection>> {
const uuid = route.queryParams.scope;
if (hasValue(uuid)) {
return this.dataService.findById(uuid).pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
map((object: Community | Collection) => {
return { provider: this.breadcrumbService, key: object, url: getDSOPath(object) };
})
);
}
return undefined;
}
}

View File

@@ -37,24 +37,24 @@ export class BrowseByGuard implements CanActivate {
return dsoAndMetadata$.pipe(
map((dsoRD) => {
const name = dsoRD.payload.name;
route.data = this.createData(title, id, metadataField, name, metadataTranslated, value);
route.data = this.createData(title, id, metadataField, name, metadataTranslated, value, route);
return true;
})
);
} else {
route.data = this.createData(title, id, metadataField, '', metadataTranslated, value);
route.data = this.createData(title, id, metadataField, '', metadataTranslated, value, route);
return observableOf(true);
}
}
private createData(title, id, metadataField, collection, field, value) {
return {
private createData(title, id, metadataField, collection, field, value, route) {
return Object.assign({}, route.data, {
title: title,
id: id,
metadataField: metadataField,
collection: collection,
field: field,
value: hasValue(value) ? `"${value}"` : ''
}
});
}
}

View File

@@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
/**
* This class resolves a BreadcrumbConfig object with an i18n key string for a route
* It adds the metadata field of the current browse-by page
*/
@Injectable()
export class BrowseByI18nBreadcrumbResolver extends I18nBreadcrumbResolver {
constructor(protected breadcrumbService: I18nBreadcrumbsService) {
super(breadcrumbService);
}
/**
* Method for resolving a browse-by i18n breadcrumb configuration object
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns BreadcrumbConfig object for a browse-by page
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig<string> {
const extendedBreadcrumbKey = route.data.breadcrumbKey + '.' + route.params.id;
route.data = Object.assign({}, route.data, { breadcrumbKey: extendedBreadcrumbKey });
return super.resolve(route, state);
}
}

View File

@@ -2,12 +2,29 @@ import { RouterModule } from '@angular/router';
import { NgModule } from '@angular/core';
import { BrowseByGuard } from './browse-by-guard';
import { BrowseBySwitcherComponent } from './+browse-by-switcher/browse-by-switcher.component';
import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver';
import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver';
@NgModule({
imports: [
RouterModule.forChild([
{ path: ':id', component: BrowseBySwitcherComponent, canActivate: [BrowseByGuard], data: { title: 'browse.title' } }
])
{
path: '',
resolve: { breadcrumb: BrowseByDSOBreadcrumbResolver },
children: [
{
path: ':id',
component: BrowseBySwitcherComponent,
canActivate: [BrowseByGuard],
resolve: { breadcrumb: BrowseByI18nBreadcrumbResolver },
data: { title: 'browse.title', breadcrumbKey: 'browse.metadata' }
}
]
}])
],
providers: [
BrowseByI18nBreadcrumbResolver,
BrowseByDSOBreadcrumbResolver
]
})
export class BrowseByRoutingModule {

View File

@@ -83,7 +83,7 @@ describe('CollectionItemMapperComponent', () => {
const itemDataServiceStub = {
mapToCollection: () => of(new RestResponse(true, 200, 'OK'))
};
const activatedRouteStub = new ActivatedRouteStub({}, { collection: mockCollectionRD });
const activatedRouteStub = new ActivatedRouteStub({}, { dso: mockCollectionRD });
const translateServiceStub = {
get: () => of('test-message of collection ' + mockCollection.name),
onLangChange: new EventEmitter(),

View File

@@ -102,7 +102,7 @@ export class CollectionItemMapperComponent implements OnInit {
}
ngOnInit(): void {
this.collectionRD$ = this.route.data.pipe(map((data) => data.collection)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Collection>>;
this.collectionRD$ = this.route.data.pipe(map((data) => data.dso)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Collection>>;
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
this.loadItemLists();
}

View File

@@ -10,6 +10,9 @@ import { DeleteCollectionPageComponent } from './delete-collection-page/delete-c
import { URLCombiner } from '../core/url-combiner/url-combiner';
import { getCollectionModulePath } from '../app-routing.module';
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
import { CollectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver';
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
import { LinkService } from '../core/cache/builders/link.service';
export const COLLECTION_PARENT_PARAMETER = 'parent';
@@ -18,7 +21,7 @@ export function getCollectionPageRoute(collectionId: string) {
}
export function getCollectionEditPath(id: string) {
return new URLCombiner(getCollectionModulePath(), COLLECTION_EDIT_PATH.replace(/:id/, id)).toString()
return new URLCombiner(getCollectionModulePath(), id, COLLECTION_EDIT_PATH).toString()
}
export function getCollectionCreatePath() {
@@ -26,51 +29,55 @@ export function getCollectionCreatePath() {
}
const COLLECTION_CREATE_PATH = 'create';
const COLLECTION_EDIT_PATH = ':id/edit';
const COLLECTION_EDIT_PATH = 'edit';
@NgModule({
imports: [
RouterModule.forChild([
{
path: COLLECTION_CREATE_PATH,
component: CreateCollectionPageComponent,
canActivate: [AuthenticatedGuard, CreateCollectionPageGuard]
path: ':id',
resolve: {
dso: CollectionPageResolver,
breadcrumb: CollectionBreadcrumbResolver
},
runGuardsAndResolvers: 'always',
children: [
{
path: COLLECTION_EDIT_PATH,
loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule',
canActivate: [AuthenticatedGuard]
},
{
path: ':id/delete',
path: 'delete',
pathMatch: 'full',
component: DeleteCollectionPageComponent,
canActivate: [AuthenticatedGuard],
resolve: {
dso: CollectionPageResolver
}
},
{
path: ':id',
path: '',
component: CollectionPageComponent,
pathMatch: 'full',
resolve: {
collection: CollectionPageResolver
}
},
{
path: ':id/edit/mapper',
path: '/edit/mapper',
component: CollectionItemMapperComponent,
pathMatch: 'full',
resolve: {
collection: CollectionPageResolver
},
canActivate: [AuthenticatedGuard]
}
]
},
{
path: COLLECTION_CREATE_PATH,
component: CreateCollectionPageComponent,
canActivate: [AuthenticatedGuard, CreateCollectionPageGuard]
},
])
],
providers: [
CollectionPageResolver,
CollectionBreadcrumbResolver,
DSOBreadcrumbsService,
LinkService,
CreateCollectionPageGuard
]
})

View File

@@ -62,7 +62,7 @@ export class CollectionPageComponent implements OnInit {
ngOnInit(): void {
this.collectionRD$ = this.route.data.pipe(
map((data) => data.collection as RemoteData<Collection>),
map((data) => data.dso as RemoteData<Collection>),
redirectToPageNotFoundOn404(this.router),
take(1)
);

View File

@@ -1,11 +1,11 @@
import { RouterModule } from '@angular/router';
import { NgModule } from '@angular/core';
import { EditCollectionPageComponent } from './edit-collection-page.component';
import { CollectionPageResolver } from '../collection-page.resolver';
import { CollectionMetadataComponent } from './collection-metadata/collection-metadata.component';
import { CollectionRolesComponent } from './collection-roles/collection-roles.component';
import { CollectionSourceComponent } from './collection-source/collection-source.component';
import { CollectionCurateComponent } from './collection-curate/collection-curate.component';
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
/**
* Routing module that handles the routing for the Edit Collection page administrator functionality
@@ -15,10 +15,11 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate
RouterModule.forChild([
{
path: '',
component: EditCollectionPageComponent,
resolve: {
dso: CollectionPageResolver
breadcrumb: I18nBreadcrumbResolver
},
data: { breadcrumbKey: 'collection.edit' },
component: EditCollectionPageComponent,
children: [
{
path: '',
@@ -30,30 +31,28 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate
component: CollectionMetadataComponent,
data: {
title: 'collection.edit.tabs.metadata.title',
hideReturnButton: true
hideReturnButton: true,
showBreadcrumbs: true
}
},
{
path: 'roles',
component: CollectionRolesComponent,
data: { title: 'collection.edit.tabs.roles.title' }
data: { title: 'collection.edit.tabs.roles.title', showBreadcrumbs: true }
},
{
path: 'source',
component: CollectionSourceComponent,
data: { title: 'collection.edit.tabs.source.title' }
data: { title: 'collection.edit.tabs.source.title', showBreadcrumbs: true }
},
{
path: 'curate',
component: CollectionCurateComponent,
data: { title: 'collection.edit.tabs.curate.title' }
data: { title: 'collection.edit.tabs.curate.title', showBreadcrumbs: true }
}
]
}
])
],
providers: [
CollectionPageResolver,
]
})
export class EditCollectionPageRoutingModule {

View File

@@ -9,6 +9,9 @@ import { CreateCommunityPageGuard } from './create-community-page/create-communi
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
import { URLCombiner } from '../core/url-combiner/url-combiner';
import { getCommunityModulePath } from '../app-routing.module';
import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver';
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
import { LinkService } from '../core/cache/builders/link.service';
export const COMMUNITY_PARENT_PARAMETER = 'parent';
@@ -17,7 +20,7 @@ export function getCommunityPageRoute(communityId: string) {
}
export function getCommunityEditPath(id: string) {
return new URLCombiner(getCommunityModulePath(), COMMUNITY_EDIT_PATH.replace(/:id/, id)).toString()
return new URLCombiner(getCommunityModulePath(), id, COMMUNITY_EDIT_PATH).toString()
}
export function getCommunityCreatePath() {
@@ -25,42 +28,49 @@ export function getCommunityCreatePath() {
}
const COMMUNITY_CREATE_PATH = 'create';
const COMMUNITY_EDIT_PATH = ':id/edit';
const COMMUNITY_EDIT_PATH = 'edit';
@NgModule({
imports: [
RouterModule.forChild([
{
path: COMMUNITY_CREATE_PATH,
component: CreateCommunityPageComponent,
canActivate: [AuthenticatedGuard, CreateCommunityPageGuard]
path: ':id',
resolve: {
dso: CommunityPageResolver,
breadcrumb: CommunityBreadcrumbResolver
},
runGuardsAndResolvers: 'always',
children: [
{
path: COMMUNITY_EDIT_PATH,
loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule',
canActivate: [AuthenticatedGuard]
},
{
path: ':id/delete',
path: 'delete',
pathMatch: 'full',
component: DeleteCommunityPageComponent,
canActivate: [AuthenticatedGuard],
resolve: {
dso: CommunityPageResolver
}
},
{
path: ':id',
path: '',
component: CommunityPageComponent,
pathMatch: 'full',
resolve: {
community: CommunityPageResolver
}
}
]
},
{
path: COMMUNITY_CREATE_PATH,
component: CreateCommunityPageComponent,
canActivate: [AuthenticatedGuard, CreateCommunityPageGuard]
},
])
],
providers: [
CommunityPageResolver,
CommunityBreadcrumbResolver,
DSOBreadcrumbsService,
LinkService,
CreateCommunityPageGuard
]
})

View File

@@ -46,7 +46,7 @@ export class CommunityPageComponent implements OnInit {
ngOnInit(): void {
this.communityRD$ = this.route.data.pipe(
map((data) => data.community as RemoteData<Community>),
map((data) => data.dso as RemoteData<Community>),
redirectToPageNotFoundOn404(this.router)
);
this.logoRD$ = this.communityRD$.pipe(

View File

@@ -5,6 +5,7 @@ import { NgModule } from '@angular/core';
import { CommunityMetadataComponent } from './community-metadata/community-metadata.component';
import { CommunityRolesComponent } from './community-roles/community-roles.component';
import { CommunityCurateComponent } from './community-curate/community-curate.component';
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
/**
* Routing module that handles the routing for the Edit Community page administrator functionality
@@ -14,10 +15,11 @@ import { CommunityCurateComponent } from './community-curate/community-curate.co
RouterModule.forChild([
{
path: '',
component: EditCommunityPageComponent,
resolve: {
dso: CommunityPageResolver
breadcrumb: I18nBreadcrumbResolver
},
data: { breadcrumbKey: 'community.edit' },
component: EditCommunityPageComponent,
children: [
{
path: '',
@@ -29,26 +31,24 @@ import { CommunityCurateComponent } from './community-curate/community-curate.co
component: CommunityMetadataComponent,
data: {
title: 'community.edit.tabs.metadata.title',
hideReturnButton: true
hideReturnButton: true,
showBreadcrumbs: true
}
},
{
path: 'roles',
component: CommunityRolesComponent,
data: { title: 'community.edit.tabs.roles.title' }
data: { title: 'community.edit.tabs.roles.title', showBreadcrumbs: true }
},
{
path: 'curate',
component: CommunityCurateComponent,
data: { title: 'community.edit.tabs.curate.title' }
data: { title: 'community.edit.tabs.curate.title', showBreadcrumbs: true }
}
]
}
])
],
providers: [
CommunityPageResolver,
]
})
export class EditCommunityPageRoutingModule {

View File

@@ -22,6 +22,7 @@ import { EditRelationshipComponent } from './item-relationships/edit-relationshi
import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component';
import { ItemMoveComponent } from './item-move/item-move.component';
import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component';
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
/**
* Module that contains all components related to the Edit Item page administrator functionality
@@ -47,6 +48,7 @@ import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.co
ItemMetadataComponent,
ItemRelationshipsComponent,
ItemBitstreamsComponent,
ItemVersionHistoryComponent,
EditInPlaceFieldComponent,
EditRelationshipComponent,
EditRelationshipListComponent,

View File

@@ -1,4 +1,3 @@
import { ItemPageResolver } from '../item-page.resolver';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { EditItemPageComponent } from './edit-item-page.component';
@@ -13,13 +12,15 @@ import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.compo
import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component';
import { ItemMoveComponent } from './item-move/item-move.component';
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { ItemVersionHistoryComponent } from './item-version-history/item-version-history.component';
const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
const ITEM_EDIT_PRIVATE_PATH = 'private';
const ITEM_EDIT_PUBLIC_PATH = 'public';
const ITEM_EDIT_DELETE_PATH = 'delete';
const ITEM_EDIT_MOVE_PATH = 'move';
export const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
export const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
export const ITEM_EDIT_PRIVATE_PATH = 'private';
export const ITEM_EDIT_PUBLIC_PATH = 'public';
export const ITEM_EDIT_DELETE_PATH = 'delete';
export const ITEM_EDIT_MOVE_PATH = 'move';
/**
* Routing module that handles the routing for the Edit Item page administrator functionality
@@ -29,10 +30,14 @@ const ITEM_EDIT_MOVE_PATH = 'move';
RouterModule.forChild([
{
path: '',
component: EditItemPageComponent,
resolve: {
item: ItemPageResolver
breadcrumb: I18nBreadcrumbResolver
},
data: { breadcrumbKey: 'item.edit' },
children: [
{
path: '',
component: EditItemPageComponent,
children: [
{
path: '',
@@ -42,91 +47,76 @@ const ITEM_EDIT_MOVE_PATH = 'move';
{
path: 'status',
component: ItemStatusComponent,
data: { title: 'item.edit.tabs.status.title' }
data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true }
},
{
path: 'bitstreams',
component: ItemBitstreamsComponent,
data: { title: 'item.edit.tabs.bitstreams.title' }
data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true }
},
{
path: 'metadata',
component: ItemMetadataComponent,
data: { title: 'item.edit.tabs.metadata.title' }
data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true }
},
{
path: 'relationships',
component: ItemRelationshipsComponent,
data: { title: 'item.edit.tabs.relationships.title' }
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true }
},
{
path: 'view',
/* TODO - change when view page exists */
component: ItemBitstreamsComponent,
data: { title: 'item.edit.tabs.view.title' }
data: { title: 'item.edit.tabs.view.title', showBreadcrumbs: true }
},
{
path: 'curate',
/* TODO - change when curate page exists */
component: ItemBitstreamsComponent,
data: { title: 'item.edit.tabs.curate.title' }
data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true }
},
{
path: 'versionhistory',
component: ItemVersionHistoryComponent,
data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true }
}
]
},
{
path: 'mapper',
component: ItemCollectionMapperComponent,
resolve: {
item: ItemPageResolver
}
},
{
path: ITEM_EDIT_WITHDRAW_PATH,
component: ItemWithdrawComponent,
resolve: {
item: ItemPageResolver
}
},
{
path: ITEM_EDIT_REINSTATE_PATH,
component: ItemReinstateComponent,
resolve: {
item: ItemPageResolver
}
},
{
path: ITEM_EDIT_PRIVATE_PATH,
component: ItemPrivateComponent,
resolve: {
item: ItemPageResolver
}
},
{
path: ITEM_EDIT_PUBLIC_PATH,
component: ItemPublicComponent,
resolve: {
item: ItemPageResolver
}
},
{
path: ITEM_EDIT_DELETE_PATH,
component: ItemDeleteComponent,
resolve: {
item: ItemPageResolver
}
},
{
path: ITEM_EDIT_MOVE_PATH,
component: ItemMoveComponent,
data: { title: 'item.edit.move.title' },
resolve: {
item: ItemPageResolver
}
}])
],
providers: [
ItemPageResolver,
]
}
])
],
providers: []
})
export class EditItemPageRoutingModule {

View File

@@ -220,7 +220,7 @@ describe('ItemDeleteComponent', () => {
spyOn(comp, 'notify');
comp.performAction();
expect(mockItemDataService.delete)
.toHaveBeenCalledWith(mockItem, types.filter((type) => typesSelection[type]).map((type) => type.id));
.toHaveBeenCalledWith(mockItem.id, types.filter((type) => typesSelection[type]).map((type) => type.id));
expect(comp.notify).toHaveBeenCalled();
});
});

View File

@@ -312,7 +312,7 @@ export class ItemDeleteComponent
)
),
).subscribe((types) => {
this.itemDataService.delete(this.item, types).pipe(first()).subscribe(
this.itemDataService.delete(this.item.id, types).pipe(first()).subscribe(
(succeeded: boolean) => {
this.notify(succeeded);
}
@@ -322,7 +322,7 @@ export class ItemDeleteComponent
/**
* When the item is successfully delete, navigate to the homepage, otherwise navigate back to the item edit page
* @param response
* @param succeeded
*/
notify(succeeded: boolean) {
if (succeeded) {

View File

@@ -0,0 +1,6 @@
<div class="mt-4">
<ds-alert [content]="'item.edit.tabs.versionhistory.under-construction'" [type]="AlertTypeEnum.Warning"></ds-alert>
</div>
<div class="mt-2" *ngVar="(itemRD$ | async)?.payload as item">
<ds-item-versions *ngIf="item" [item]="item" [displayWhenEmpty]="true" [displayTitle]="false"></ds-item-versions>
</div>

View File

@@ -0,0 +1,44 @@
import { ItemVersionHistoryComponent } from './item-version-history.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { VarDirective } from '../../../shared/utils/var.directive';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Item } from '../../../core/shared/item.model';
import { ActivatedRoute } from '@angular/router';
import { of as observableOf } from 'rxjs';
import { createSuccessfulRemoteDataObject } from '../../../shared/testing/utils';
describe('ItemVersionHistoryComponent', () => {
let component: ItemVersionHistoryComponent;
let fixture: ComponentFixture<ItemVersionHistoryComponent>;
const item = Object.assign(new Item(), {
uuid: 'item-identifier-1',
handle: '123456789/1',
});
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ItemVersionHistoryComponent, VarDirective],
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
providers: [
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ item: createSuccessfulRemoteDataObject(item) }) } } }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ItemVersionHistoryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should initialize the itemRD$ from the route\'s data', (done) => {
component.itemRD$.subscribe((itemRD) => {
expect(itemRD.payload).toBe(item);
done();
});
});
});

View File

@@ -0,0 +1,35 @@
import { Component } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../../core/data/remote-data';
import { Item } from '../../../core/shared/item.model';
import { map } from 'rxjs/operators';
import { getSucceededRemoteData } from '../../../core/shared/operators';
import { ActivatedRoute } from '@angular/router';
import { AlertType } from '../../../shared/alert/aletr-type';
@Component({
selector: 'ds-item-version-history',
templateUrl: './item-version-history.component.html'
})
/**
* Component for listing and managing an item's version history
*/
export class ItemVersionHistoryComponent {
/**
* The item to display the version history for
*/
itemRD$: Observable<RemoteData<Item>>;
/**
* The AlertType enumeration
* @type {AlertType}
*/
AlertTypeEnum = AlertType;
constructor(private route: ActivatedRoute) {
}
ngOnInit(): void {
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.item)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Item>>;
}
}

View File

@@ -1,6 +1,7 @@
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
<div *ngIf="itemRD?.payload as item">
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
<ds-view-tracker [object]="item"></ds-view-tracker>
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
<div class="simple-view-link my-3">
@@ -21,6 +22,7 @@
</table>
<ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section>
<ds-item-page-collections [item]="item"></ds-item-page-collections>
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
</div>
</div>
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>

View File

@@ -7,44 +7,56 @@ import { ItemPageResolver } from './item-page.resolver';
import { URLCombiner } from '../core/url-combiner/url-combiner';
import { getItemModulePath } from '../app-routing.module';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
import { ItemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver';
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
import { LinkService } from '../core/cache/builders/link.service';
export function getItemPageRoute(itemId: string) {
return new URLCombiner(getItemModulePath(), itemId).toString();
}
export function getItemEditPath(id: string) {
return new URLCombiner(getItemModulePath(),ITEM_EDIT_PATH.replace(/:id/, id)).toString()
return new URLCombiner(getItemModulePath(), id, ITEM_EDIT_PATH).toString()
}
const ITEM_EDIT_PATH = ':id/edit';
const ITEM_EDIT_PATH = 'edit';
@NgModule({
imports: [
RouterModule.forChild([
{
path: ':id',
resolve: {
item: ItemPageResolver,
breadcrumb: ItemBreadcrumbResolver
},
runGuardsAndResolvers: 'always',
children: [
{
path: '',
component: ItemPageComponent,
pathMatch: 'full',
resolve: {
item: ItemPageResolver
}
},
{
path: ':id/full',
path: 'full',
component: FullItemPageComponent,
resolve: {
item: ItemPageResolver
}
},
{
path: ITEM_EDIT_PATH,
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
canActivate: [AuthenticatedGuard]
},
}
],
}
])
],
providers: [
ItemPageResolver,
ItemBreadcrumbResolver,
DSOBreadcrumbsService,
LinkService
]
})
export class ItemPageRoutingModule {

View File

@@ -59,7 +59,7 @@ import { AbstractIncrementalListComponent } from './simple/abstract-incremental-
MetadataRepresentationListComponent,
RelatedEntitiesSearchComponent,
TabbedRelatedEntitiesSearchComponent,
AbstractIncrementalListComponent
AbstractIncrementalListComponent,
],
exports: [
ItemComponent,

View File

@@ -27,7 +27,8 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
return this.itemService.findById(route.params.id,
followLink('owningCollection'),
followLink('bundles'),
followLink('relationships')
followLink('relationships'),
followLink('version', undefined, true, followLink('versionhistory')),
).pipe(
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
);

View File

@@ -1,8 +1,10 @@
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
<div *ngIf="itemRD?.payload as item">
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
<ds-view-tracker [object]="item"></ds-view-tracker>
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
</div>
</div>
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.item' | translate}}"></ds-error>

View File

@@ -2,12 +2,14 @@ import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { LoginPageComponent } from './login-page.component';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
@NgModule({
imports: [
RouterModule.forChild([
{ path: '', pathMatch: 'full', component: LoginPageComponent, data: { title: 'login.title' } }
{ path: '', pathMatch: 'full', component: LoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' } }
])
]
})
export class LoginPageRoutingModule { }
export class LoginPageRoutingModule {
}

View File

@@ -19,7 +19,7 @@
<ds-search-labels [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
<div class="row">
<div id="search-body"
class="row-offcanvas row-offcanvas-left"
class="row-offcanvas row-offcanvas-left w-100"
[@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
id="search-sidebar-sm"

View File

@@ -1,3 +1,4 @@
import { switchMap } from 'rxjs/operators';
import { HostWindowService } from '../shared/host-window.service';
import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SearchComponent } from './search.component';

View File

@@ -4,13 +4,24 @@ import { RouterModule } from '@angular/router';
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
import { SearchPageComponent } from './search-page.component';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
@NgModule({
imports: [
RouterModule.forChild([
{ path: '', component: SearchPageComponent, data: { title: 'search.title' } },
RouterModule.forChild([{
path: '',
resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'search' },
children: [
{ path: '', component: SearchPageComponent },
{ path: ':configuration', component: ConfigurationSearchPageComponent, canActivate: [ConfigurationSearchPageGuard] }
])
]
}]
)
],
providers: [
I18nBreadcrumbResolver,
I18nBreadcrumbsService
]
})
export class SearchPageRoutingModule {

View File

@@ -14,6 +14,7 @@ import { SearchPageComponent } from './search-page.component';
import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service';
import { SearchFilterService } from '../core/shared/search/search-filter.service';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { TranslateModule } from '@ngx-translate/core';
const components = [
SearchPageComponent,
@@ -28,7 +29,7 @@ const components = [
CommonModule,
SharedModule,
CoreModule.forRoot(),
StatisticsModule.forRoot(),
StatisticsModule.forRoot()
],
declarations: components,
providers: [

View File

@@ -22,7 +22,8 @@
<ds-search-results [searchResults]="resultsRD$ | async"
[searchConfig]="searchOptions$ | async"
[configuration]="configuration$ | async"
[disableHeader]="!searchEnabled"></ds-search-results>
[disableHeader]="!searchEnabled"
[context]="context"></ds-search-results>
</div>
</div>
</ds-page-with-sidebar>

View File

@@ -17,6 +17,7 @@ import { SearchConfigurationService } from '../core/shared/search/search-configu
import { SearchService } from '../core/shared/search/search.service';
import { currentPath } from '../shared/utils/route.utils';
import { Router } from '@angular/router';
import { Context } from '../core/shared/context.model';
@Component({
selector: 'ds-search',
@@ -84,6 +85,12 @@ export class SearchComponent implements OnInit {
@Input()
configuration$: Observable<string>;
/**
* The current context
*/
@Input()
context: Context;
/**
* Link to the search page
*/

View File

@@ -3,49 +3,96 @@ import { RouterModule } from '@angular/router';
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
import { DSpaceObject } from './core/shared/dspace-object.model';
import { Community } from './core/shared/community.model';
import { getCommunityPageRoute } from './+community-page/community-page-routing.module';
import { Collection } from './core/shared/collection.model';
import { Item } from './core/shared/item.model';
import { getItemPageRoute } from './+item-page/item-page-routing.module';
import { getCollectionPageRoute } from './+collection-page/collection-page-routing.module';
const ITEM_MODULE_PATH = 'items';
export function getItemModulePath() {
return `/${ITEM_MODULE_PATH}`;
}
const COLLECTION_MODULE_PATH = 'collections';
export function getCollectionModulePath() {
return `/${COLLECTION_MODULE_PATH}`;
}
const COMMUNITY_MODULE_PATH = 'communities';
export function getCommunityModulePath() {
return `/${COMMUNITY_MODULE_PATH}`;
}
const ADMIN_MODULE_PATH = 'admin';
export function getAdminModulePath() {
return `/${ADMIN_MODULE_PATH}`;
}
const PROFILE_MODULE_PATH = 'profile';
export function getProfileModulePath() {
return `/${PROFILE_MODULE_PATH}`;
}
export function getDSOPath(dso: DSpaceObject): string {
switch ((dso as any).type) {
case Community.type.value:
return getCommunityPageRoute(dso.uuid);
case Collection.type.value:
return getCollectionPageRoute(dso.uuid);
case Item.type.value:
return getItemPageRoute(dso.uuid);
}
}
@NgModule({
imports: [
RouterModule.forRoot([
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' },
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule', data: { showBreadcrumbs: false } },
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' },
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' },
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
{ path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' },
{ path: 'mydspace', loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', canActivate: [AuthenticatedGuard] },
{
path: 'mydspace',
loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule',
canActivate: [AuthenticatedGuard]
},
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'},
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
{ path: 'workspaceitems', loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' },
{ path: 'workflowitems', loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule' },
{
path: 'workspaceitems',
loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule'
},
{
path: 'workflowitems',
loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule'
},
{
path: PROFILE_MODULE_PATH,
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard]
},
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
])
],
exports: [RouterModule]
{
onSameUrlNavigation: 'reload',
})
],
exports: [RouterModule],
})
export class AppRoutingModule {

View File

@@ -10,6 +10,10 @@
[options]="config.notifications">
</ds-notifications-board>
<main class="main-content">
<div class="container">
<ds-breadcrumbs></ds-breadcrumbs>
</div>
<div class="container" *ngIf="isLoading$ | async">
<ds-loading message="{{'loading.default' | translate}}"></ds-loading>
</div>

View File

@@ -3,12 +3,11 @@ import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { EffectsModule } from '@ngrx/effects';
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
import { META_REDUCERS, MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { DYNAMIC_MATCHER_PROVIDERS } from '@ng-dynamic-forms/core';
import { TranslateModule } from '@ngx-translate/core';
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
@@ -21,7 +20,7 @@ import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { appEffects } from './app.effects';
import { appMetaReducers, debugMetaReducers, universalMetaReducer } from './app.metareducers';
import { appMetaReducers, debugMetaReducers } from './app.metareducers';
import { appReducers, AppState } from './app.reducer';
import { CoreModule } from './core/core.module';
@@ -39,6 +38,7 @@ import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-s
import { NotificationComponent } from './shared/notifications/notification/notification.component';
import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component';
import { SharedModule } from './shared/shared.module';
import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component';
export function getConfig() {
return ENV_CONFIG;
@@ -97,7 +97,8 @@ const PROVIDERS = [
provide: RouterStateSerializer,
useClass: DSpaceRouterStateSerializer
},
ClientCookieService
ClientCookieService,
...DYNAMIC_MATCHER_PROVIDERS,
];
const DECLARATIONS = [
@@ -128,6 +129,7 @@ const EXPORTS = [
],
declarations: [
...DECLARATIONS,
BreadcrumbsComponent,
],
exports: [
...EXPORTS

View File

@@ -1,30 +1,34 @@
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
import * as fromRouter from '@ngrx/router-store';
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer';
import { formReducer, FormState } from './shared/form/form.reducer';
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
import { sidebarFilterReducer, SidebarFiltersState } from './shared/sidebar/filter/sidebar-filter.reducer';
import { filterReducer, SearchFiltersState } from './shared/search/search-filters/search-filter/search-filter.reducer';
import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers';
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
import {
ePeopleRegistryReducer,
EPeopleRegistryState
} from './+admin/admin-access-control/epeople-registry/epeople-registry.reducers';
import {
metadataRegistryReducer,
MetadataRegistryState
} from './+admin/admin-registries/metadata-registry/metadata-registry.reducers';
import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer';
import { hasValue } from './shared/empty.util';
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
import {
NameVariantListsState,
nameVariantReducer
} from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
import { formReducer, FormState } from './shared/form/form.reducer';
import { menusReducer, MenusState } from './shared/menu/menu.reducer';
import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers';
import {
selectableListReducer,
SelectableListsState
} from './shared/object-list/selectable-list/selectable-list.reducer';
import { ObjectSelectionListState, objectSelectionReducer } from './shared/object-select/object-select.reducer';
import {
NameVariantListsState,
nameVariantReducer
} from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
import { filterReducer, SearchFiltersState } from './shared/search/search-filters/search-filter/search-filter.reducer';
import { sidebarFilterReducer, SidebarFiltersState } from './shared/sidebar/filter/sidebar-filter.reducer';
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
export interface AppState {
router: fromRouter.RouterReducerState;
@@ -42,6 +46,7 @@ export interface AppState {
selectableLists: SelectableListsState;
relationshipLists: NameVariantListsState;
communityList: CommunityListState;
epeopleRegistry: EPeopleRegistryState;
}
export const appReducers: ActionReducerMap<AppState> = {
@@ -60,6 +65,7 @@ export const appReducers: ActionReducerMap<AppState> = {
selectableLists: selectableListReducer,
relationshipLists: nameVariantReducer,
communityList: CommunityListReducer,
epeopleRegistry: ePeopleRegistryReducer,
};
export const routerStateSelector = (state: AppState) => state.router;

View File

@@ -0,0 +1,21 @@
import { BreadcrumbsService } from '../../core/breadcrumbs/breadcrumbs.service';
/**
* Interface for breadcrumb configuration objects
*/
export interface BreadcrumbConfig<T> {
/**
* The service used to calculate the breadcrumb object
*/
provider: BreadcrumbsService<T>;
/**
* The key that is used to calculate the breadcrumb display value
*/
key: T;
/**
* The url of the breadcrumb
*/
url?: string;
}

View File

@@ -0,0 +1,15 @@
/**
* Class representing a single breadcrumb
*/
export class Breadcrumb {
constructor(
/**
* The display value of the breadcrumb
*/
public text: string,
/**
* The optional url of the breadcrumb
*/
public url?: string) {
}
}

View File

@@ -0,0 +1,17 @@
<nav *ngIf="showBreadcrumbs" aria-label="breadcrumb">
<ol class="breadcrumb">
<ng-container *ngTemplateOutlet="breadcrumbs.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'Home', url: '/'}"></ng-container>
<ng-container *ngFor="let bc of breadcrumbs; let last = last;">
<ng-container *ngTemplateOutlet="!last ? breadcrumb : activeBreadcrumb; context: bc"></ng-container>
</ng-container>
</ol>
</nav>
<ng-template #breadcrumb let-text="text" let-url="url">
<li class="breadcrumb-item"><a [routerLink]="url">{{text | translate}}</a></li>
</ng-template>
<ng-template #activeBreadcrumb let-text="text" >
<li class="breadcrumb-item active" aria-current="page">{{text | translate}}</li>
</ng-template>

View File

@@ -0,0 +1,111 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { BreadcrumbsComponent } from './breadcrumbs.component';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { Observable, of as observableOf } from 'rxjs';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { MockTranslateLoader } from '../shared/testing/mock-translate-loader';
import { BreadcrumbConfig } from './breadcrumb/breadcrumb-config.model';
import { BreadcrumbsService } from '../core/breadcrumbs/breadcrumbs.service';
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
import { getTestScheduler } from 'jasmine-marbles';
class TestBreadcrumbsService implements BreadcrumbsService<string> {
getBreadcrumbs(key: string, url: string): Observable<Breadcrumb[]> {
return observableOf([new Breadcrumb(key, url)]);
}
}
describe('BreadcrumbsComponent', () => {
let component: BreadcrumbsComponent;
let fixture: ComponentFixture<BreadcrumbsComponent>;
let router: any;
let route: any;
let breadcrumbProvider;
let breadcrumbConfigA: BreadcrumbConfig<string>;
let breadcrumbConfigB: BreadcrumbConfig<string>;
let expectedBreadcrumbs;
function init() {
breadcrumbProvider = new TestBreadcrumbsService();
breadcrumbConfigA = { provider: breadcrumbProvider, key: 'example.path', url: 'example.com' };
breadcrumbConfigB = { provider: breadcrumbProvider, key: 'another.path', url: 'another.com' };
route = {
root: {
snapshot: {
data: { breadcrumb: breadcrumbConfigA },
routeConfig: { resolve: { breadcrumb: {} } }
},
firstChild: {
snapshot: {
// Example without resolver should be ignored
data: { breadcrumb: breadcrumbConfigA },
},
firstChild: {
snapshot: {
data: { breadcrumb: breadcrumbConfigB },
routeConfig: { resolve: { breadcrumb: {} } }
}
}
}
}
};
expectedBreadcrumbs = [
new Breadcrumb(breadcrumbConfigA.key, breadcrumbConfigA.url),
new Breadcrumb(breadcrumbConfigB.key, breadcrumbConfigB.url)
]
}
beforeEach(async(() => {
init();
TestBed.configureTestingModule({
declarations: [BreadcrumbsComponent],
imports: [RouterTestingModule.withRoutes([]), TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
})],
providers: [
{ provide: ActivatedRoute, useValue: route }
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BreadcrumbsComponent);
component = fixture.componentInstance;
router = TestBed.get(Router);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('ngOnInit', () => {
beforeEach(() => {
spyOn(component, 'resolveBreadcrumbs').and.returnValue(observableOf([]))
});
it('should call resolveBreadcrumb on init', () => {
router.events = observableOf(new NavigationEnd(0, '', ''));
component.ngOnInit();
expect(component.resolveBreadcrumbs).toHaveBeenCalledWith(route.root);
})
});
describe('resolveBreadcrumbs', () => {
it('should return the correct breadcrumbs', () => {
const breadcrumbs = component.resolveBreadcrumbs(route.root);
getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: expectedBreadcrumbs })
})
})
});

View File

@@ -0,0 +1,100 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { Breadcrumb } from './breadcrumb/breadcrumb.model';
import { hasNoValue, hasValue, isNotUndefined, isUndefined } from '../shared/empty.util';
import { filter, map, switchMap, tap } from 'rxjs/operators';
import { combineLatest, Observable, Subscription, of as observableOf } from 'rxjs';
/**
* Component representing the breadcrumbs of a page
*/
@Component({
selector: 'ds-breadcrumbs',
templateUrl: './breadcrumbs.component.html',
styleUrls: ['./breadcrumbs.component.scss']
})
export class BreadcrumbsComponent implements OnInit, OnDestroy {
/**
* List of breadcrumbs for this page
*/
breadcrumbs: Breadcrumb[];
/**
* Whether or not to show breadcrumbs on this page
*/
showBreadcrumbs: boolean;
/**
* Subscription to unsubscribe from on destroy
*/
subscription: Subscription;
constructor(
private route: ActivatedRoute,
private router: Router
) {
}
/**
* Sets the breadcrumbs on init for this page
*/
ngOnInit(): void {
this.subscription = this.router.events.pipe(
filter((e): e is NavigationEnd => e instanceof NavigationEnd),
tap(() => this.reset()),
switchMap(() => this.resolveBreadcrumbs(this.route.root))
).subscribe((breadcrumbs) => {
this.breadcrumbs = breadcrumbs;
}
)
}
/**
* Method that recursively resolves breadcrumbs
* @param route The route to get the breadcrumb from
*/
resolveBreadcrumbs(route: ActivatedRoute): Observable<Breadcrumb[]> {
const data = route.snapshot.data;
const routeConfig = route.snapshot.routeConfig;
const last: boolean = hasNoValue(route.firstChild);
if (last) {
if (hasValue(data.showBreadcrumbs)) {
this.showBreadcrumbs = data.showBreadcrumbs;
} else if (isUndefined(data.breadcrumb)) {
this.showBreadcrumbs = false;
}
}
if (
hasValue(data) && hasValue(data.breadcrumb) &&
hasValue(routeConfig) && hasValue(routeConfig.resolve) && hasValue(routeConfig.resolve.breadcrumb)
) {
const { provider, key, url } = data.breadcrumb;
if (!last) {
return combineLatest(provider.getBreadcrumbs(key, url), this.resolveBreadcrumbs(route.firstChild))
.pipe(map((crumbs) => [].concat.apply([], crumbs)));
} else {
return provider.getBreadcrumbs(key, url);
}
}
return !last ? this.resolveBreadcrumbs(route.firstChild) : observableOf([]);
}
/**
* Unsubscribe from subscription
*/
ngOnDestroy(): void {
if (hasValue(this.subscription)) {
this.subscription.unsubscribe();
}
}
/**
* Resets the state of the breadcrumbs
*/
reset() {
this.breadcrumbs = [];
this.showBreadcrumbs = true;
}
}

View File

@@ -1,7 +1,6 @@
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
import { Inject, Injectable } from '@angular/core';
import { EPersonDataService } from '../eperson/eperson-data.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestService } from '../data/request.service';
import { GLOBAL_CONFIG } from '../../../config';
@@ -11,6 +10,7 @@ import { AuthGetRequest, AuthPostRequest, GetRequest, PostRequest, RestRequest }
import { AuthStatusResponse, ErrorResponse } from '../cache/response.models';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { getResponseFromEntry } from '../shared/operators';
import { HttpClient } from '@angular/common/http';
@Injectable()
export class AuthRequestService {
@@ -19,7 +19,8 @@ export class AuthRequestService {
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected halService: HALEndpointService,
protected requestService: RequestService) {
protected requestService: RequestService,
private http: HttpClient) {
}
protected fetchRequest(request: RestRequest): Observable<any> {
@@ -39,7 +40,7 @@ export class AuthRequestService {
return isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`;
}
public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable<any> {
public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable<any> {
return this.halService.getEndpoint(this.linkName).pipe(
filter((href: string) => isNotEmpty(href)),
map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
@@ -68,4 +69,5 @@ export class AuthRequestService {
mergeMap((request: GetRequest) => this.fetchRequest(request)),
distinctUntilChanged());
}
}

Some files were not shown because too many files have changed in this diff Show More