mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge remote-tracking branch 'upstream/main' into w2p-74199_Admin-search-dialogs
This commit is contained in:
87
.github/workflows/build.yml
vendored
Normal file
87
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
# DSpace Continuous Integration/Build via GitHub Actions
|
||||
# Concepts borrowed from
|
||||
# https://docs.github.com/en/free-pro-team@latest/actions/guides/building-and-testing-nodejs
|
||||
name: Build
|
||||
|
||||
# Run this Build for all pushes / PRs to current branch
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# The ci step will test the dspace-angular code against DSpace REST.
|
||||
# Direct that step to utilize a DSpace REST service that has been started in docker.
|
||||
DSPACE_REST_HOST: localhost
|
||||
DSPACE_REST_PORT: 8080
|
||||
DSPACE_REST_NAMESPACE: '/server'
|
||||
DSPACE_REST_SSL: false
|
||||
strategy:
|
||||
# Create a matrix of Node versions to test against (in parallel)
|
||||
matrix:
|
||||
node-version: [10.x, 12.x]
|
||||
# Do NOT exit immediately if one matrix job fails
|
||||
fail-fast: false
|
||||
# These are the actual CI steps to perform per job
|
||||
steps:
|
||||
# https://github.com/actions/checkout
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v1
|
||||
|
||||
# https://github.com/actions/setup-node
|
||||
- name: Install Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install latest Chrome (for e2e tests)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get --only-upgrade install google-chrome-stable -y
|
||||
google-chrome --version
|
||||
|
||||
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
|
||||
- name: Get Yarn cache directory
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- name: Cache Yarn dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
# Cache entire Yarn cache directory (see previous step)
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
# Cache key is hash of yarn.lock. Therefore changes to yarn.lock will invalidate cache
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install Yarn dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Run lint
|
||||
run: yarn run lint
|
||||
|
||||
- name: Run build
|
||||
run: yarn run build:prod
|
||||
|
||||
- name: Run specs (unit tests)
|
||||
run: yarn run test:headless
|
||||
|
||||
# Using docker-compose start backend using CI configuration
|
||||
# and load assetstore from a cached copy
|
||||
- name: Start DSpace REST Backend via Docker (for e2e tests)
|
||||
run: |
|
||||
docker-compose -f ./docker/docker-compose-ci.yml up -d
|
||||
docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
|
||||
docker container ls
|
||||
|
||||
- name: Run e2e tests (integration tests)
|
||||
run: yarn run e2e:ci
|
||||
|
||||
- name: Shutdown Docker containers
|
||||
run: docker-compose -f ./docker/docker-compose-ci.yml down
|
||||
|
||||
# NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286
|
||||
# Upload coverage reports to Codecov (for Node v12 only)
|
||||
# https://github.com/codecov/codecov-action
|
||||
- name: Upload coverage to Codecov.io
|
||||
uses: codecov/codecov-action@v1
|
||||
if: matrix.node-version == '12.x'
|
66
.travis.yml
66
.travis.yml
@@ -1,66 +0,0 @@
|
||||
os: linux
|
||||
dist: bionic
|
||||
language: node_js
|
||||
|
||||
# Enable caching for yarn & node_modules
|
||||
cache:
|
||||
yarn: true
|
||||
|
||||
node_js:
|
||||
- "10"
|
||||
- "12"
|
||||
|
||||
# Install latest chrome (for e2e headless testing). Run an update if needed.
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- google-chrome
|
||||
packages:
|
||||
- google-chrome-stable
|
||||
update: true
|
||||
|
||||
env:
|
||||
# The ci step will test the dspace-angular code against DSpace REST.
|
||||
# Direct that step to utilize a DSpace REST service that has been started in docker.
|
||||
DSPACE_REST_HOST: localhost
|
||||
DSPACE_REST_PORT: 8080
|
||||
DSPACE_REST_NAMESPACE: '/server'
|
||||
DSPACE_REST_SSL: false
|
||||
|
||||
before_install:
|
||||
# Check our versions of everything
|
||||
- echo "Check versions"
|
||||
- yarn -v
|
||||
- docker-compose -v
|
||||
- google-chrome-stable --version
|
||||
|
||||
install:
|
||||
# Start up a test DSpace 7 REST backend using the entities database dump
|
||||
- docker-compose -f ./docker/docker-compose-travis.yml up -d
|
||||
# Use the dspace-cli image to populate the assetstore. Triggers a discovery and oai update
|
||||
- docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
|
||||
# Install all local dependencies (retry if initially fails)
|
||||
- travis_retry yarn install
|
||||
|
||||
before_script:
|
||||
- echo "Check Docker containers"
|
||||
- docker container ls
|
||||
# The following line could be enabled to verify that the rest server is responding.
|
||||
#- echo "Check REST API available (via Docker)"
|
||||
#- curl http://localhost:8080/server/
|
||||
|
||||
script:
|
||||
# build app and run all tests
|
||||
- ng lint || travis_terminate 1;
|
||||
- travis_wait yarn run build:prod || travis_terminate 1;
|
||||
- yarn test:headless || travis_terminate 1;
|
||||
- yarn run e2e:ci || travis_terminate 1;
|
||||
|
||||
after_script:
|
||||
# Shutdown docker after everything runs
|
||||
- docker-compose -f ./docker/docker-compose-travis.yml down
|
||||
|
||||
# After a successful build and test (see 'script'), send code coverage reports to codecov.io
|
||||
# NOTE: As there's no need to send coverage multiple times, we only run this for one version of node.
|
||||
after_success:
|
||||
- if [ "$TRAVIS_NODE_VERSION" = "12" ]; then bash <(curl -s https://codecov.io/bash); fi
|
@@ -1,4 +1,4 @@
|
||||
[](https://travis-ci.com/DSpace/dspace-angular) [](https://codecov.io/gh/DSpace/dspace-angular) [](https://github.com/angular/universal)
|
||||
[](https://github.com/DSpace/dspace-angular/actions?query=workflow%3ABuild) [](https://codecov.io/gh/DSpace/dspace-angular) [](https://github.com/angular/universal)
|
||||
|
||||
dspace-angular
|
||||
==============
|
||||
|
@@ -1,3 +1,4 @@
|
||||
# Docker Compose for running the DSpace backend for e2e testing in CI
|
||||
networks:
|
||||
dspacenet:
|
||||
services:
|
@@ -13,12 +13,12 @@ import { GROUP_EDIT_PATH } from './admin-access-control-routing-paths';
|
||||
{
|
||||
path: `${GROUP_EDIT_PATH}/:groupId`,
|
||||
component: GroupFormComponent,
|
||||
data: {title: 'admin.registries.schema.title'}
|
||||
data: {title: 'admin.access-control.groups.title.singleGroup'}
|
||||
},
|
||||
{
|
||||
path: `${GROUP_EDIT_PATH}/newGroup`,
|
||||
component: GroupFormComponent,
|
||||
data: {title: 'admin.registries.schema.title'}
|
||||
data: {title: 'admin.access-control.groups.title.addGroup'}
|
||||
},
|
||||
])
|
||||
]
|
||||
|
@@ -40,7 +40,7 @@
|
||||
</form>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(ePeopleDto$ | async)?.totalElements > 0"
|
||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="pageInfoState$"
|
||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||
|
@@ -44,7 +44,7 @@ describe('EPeopleRegistryComponent', () => {
|
||||
activeEPerson: null,
|
||||
allEpeople: mockEPeople,
|
||||
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, currentPage: 1 }), this.allEpeople));
|
||||
},
|
||||
getActiveEPerson(): Observable<EPerson> {
|
||||
return observableOf(this.activeEPerson);
|
||||
@@ -54,18 +54,18 @@ describe('EPeopleRegistryComponent', () => {
|
||||
const result = this.allEpeople.find((ePerson: EPerson) => {
|
||||
return ePerson.email === query
|
||||
});
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: [result].length, totalElements: [result].length, totalPages: 1, currentPage: 1 }), [result]));
|
||||
}
|
||||
if (scope === 'metadata') {
|
||||
if (query === '') {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, currentPage: 1 }), this.allEpeople));
|
||||
}
|
||||
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(new PageInfo({ elementsPerPage: [result].length, totalElements: [result].length, totalPages: 1, currentPage: 1 }), [result]));
|
||||
}
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, currentPage: 1 }), this.allEpeople));
|
||||
},
|
||||
deleteEPerson(ePerson: EPerson): Observable<boolean> {
|
||||
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
|
||||
|
@@ -141,7 +141,8 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
currentPage: this.config.currentPage,
|
||||
elementsPerPage: this.config.pageSize
|
||||
}).subscribe((peopleRD) => {
|
||||
this.ePeople$.next(peopleRD)
|
||||
this.ePeople$.next(peopleRD);
|
||||
this.pageInfoState$.next(peopleRD.payload.pageInfo);
|
||||
}
|
||||
));
|
||||
|
||||
|
@@ -1,6 +1,11 @@
|
||||
<div class="container">
|
||||
<div class="group-form row">
|
||||
<div class="col-12">
|
||||
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"
|
||||
[content]="messagePrefix + '.alert.permanent'"></ds-alert>
|
||||
<ds-alert *ngIf="!(canEdit$ | async) && (groupDataService.getActiveGroup() | async)" [type]="AlertTypeEnum.Warning"
|
||||
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: (getLinkedDSO(groupBeingEdited) | async)?.payload?.name, comcol: (getLinkedDSO(groupBeingEdited) | async)?.payload?.type, comcolEditRolesRoute: (getLinkedEditRolesRoute(groupBeingEdited) | async) })">
|
||||
</ds-alert>
|
||||
|
||||
<div *ngIf="groupDataService.getActiveGroup() | async; then editheader; else createHeader"></div>
|
||||
|
||||
@@ -18,10 +23,18 @@
|
||||
[formLayout]="formLayout"
|
||||
(cancel)="onCancel()"
|
||||
(submitForm)="onSubmit()">
|
||||
<div *ngIf="groupBeingEdited != null" class="row">
|
||||
<button class="btn btn-light delete-button" [disabled]="!(canEdit$ | async) || groupBeingEdited.permanent"
|
||||
(click)="delete()">
|
||||
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</ds-form>
|
||||
|
||||
<ds-members-list *ngIf="groupBeingEdited != null" [messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
|
||||
<ds-subgroups-list *ngIf="groupBeingEdited != null" [messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
|
||||
<ds-members-list *ngIf="groupBeingEdited != null"
|
||||
[messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
|
||||
<ds-subgroups-list *ngIf="groupBeingEdited != null"
|
||||
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
|
||||
|
||||
<div>
|
||||
<button [routerLink]="[this.groupDataService.getGroupRegistryRouterLink()]"
|
||||
|
@@ -14,11 +14,14 @@ import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-d
|
||||
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||
import { RestResponse } from '../../../../core/cache/response.models';
|
||||
import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service';
|
||||
import { DSpaceObjectDataService } from '../../../../core/data/dspace-object-data.service';
|
||||
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||
import { Group } from '../../../../core/eperson/models/group.model';
|
||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||
import { UUIDService } from '../../../../core/shared/uuid.service';
|
||||
@@ -40,6 +43,9 @@ describe('GroupFormComponent', () => {
|
||||
let builderService: FormBuilderService;
|
||||
let ePersonDataServiceStub: any;
|
||||
let groupsDataServiceStub: any;
|
||||
let dsoDataServiceStub: any;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let notificationService: NotificationsServiceStub;
|
||||
let router;
|
||||
|
||||
let groups;
|
||||
@@ -74,6 +80,9 @@ describe('GroupFormComponent', () => {
|
||||
editGroup(group: Group) {
|
||||
this.activeGroup = group
|
||||
},
|
||||
updateGroup(group: Group) {
|
||||
return null;
|
||||
},
|
||||
cancelEditGroup(): void {
|
||||
this.activeGroup = null;
|
||||
},
|
||||
@@ -88,9 +97,18 @@ describe('GroupFormComponent', () => {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), []))
|
||||
}
|
||||
};
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true)
|
||||
});
|
||||
dsoDataServiceStub = {
|
||||
findByHref(href: string): Observable<RemoteData<DSpaceObject>> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
router = new RouterMock();
|
||||
notificationService = new NotificationsServiceStub();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
@@ -104,7 +122,8 @@ describe('GroupFormComponent', () => {
|
||||
providers: [GroupFormComponent,
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
{ provide: GroupDataService, useValue: groupsDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: DSpaceObjectDataService, useValue: dsoDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: notificationService },
|
||||
{ provide: FormBuilderService, useValue: builderService },
|
||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||
{ provide: HttpClient, useValue: {} },
|
||||
@@ -115,6 +134,7 @@ describe('GroupFormComponent', () => {
|
||||
{ provide: HALEndpointService, useValue: {} },
|
||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }), params: observableOf({}) } },
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
@@ -148,6 +168,34 @@ describe('GroupFormComponent', () => {
|
||||
});
|
||||
}));
|
||||
});
|
||||
describe('with active Group', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf(expected));
|
||||
spyOn(groupsDataServiceStub, 'updateGroup').and.returnValue(observableOf(new RestResponse(true, 200, 'OK')));
|
||||
component.groupName.value = 'newGroupName';
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit the existing group using the correct new values', async(() => {
|
||||
const expected2 = Object.assign(new Group(), {
|
||||
name: 'newGroupName',
|
||||
metadata: {
|
||||
'dc.description': [
|
||||
{
|
||||
value: groupDescription
|
||||
}
|
||||
],
|
||||
},
|
||||
});
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
||||
});
|
||||
}));
|
||||
it('should emit success notification', () => {
|
||||
expect(notificationService.success).toHaveBeenCalled();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
DynamicFormControlModel,
|
||||
DynamicFormLayout,
|
||||
@@ -8,18 +9,30 @@ import {
|
||||
DynamicTextAreaModel
|
||||
} 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 { ObservedValueOf, combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { catchError, map, switchMap, take } from 'rxjs/operators';
|
||||
import { getCollectionEditRolesRoute } from '../../../../+collection-page/collection-page-routing-paths';
|
||||
import { getCommunityEditRolesRoute } from '../../../../+community-page/community-page-routing-paths';
|
||||
import { RestResponse } from '../../../../core/cache/response.models';
|
||||
import { DSpaceObjectDataService } from '../../../../core/data/dspace-object-data.service';
|
||||
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||
import { Group } from '../../../../core/eperson/models/group.model';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { Community } from '../../../../core/shared/community.model';
|
||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||
import { AlertType } from '../../../../shared/alert/aletr-type';
|
||||
import { ConfirmationModalComponent } from '../../../../shared/confirmation-modal/confirmation-modal.component';
|
||||
import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-group-form',
|
||||
@@ -89,22 +102,51 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
groupBeingEdited: Group;
|
||||
|
||||
/**
|
||||
* Observable whether or not the logged in user is allowed to delete the Group & doesn't have a linked object (community / collection linked to workspace group
|
||||
*/
|
||||
canEdit$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The AlertType enumeration
|
||||
* @type {AlertType}
|
||||
*/
|
||||
public AlertTypeEnum = AlertType;
|
||||
|
||||
constructor(public groupDataService: GroupDataService,
|
||||
private ePersonDataService: EPersonDataService,
|
||||
private dSpaceObjectDataService: DSpaceObjectDataService,
|
||||
private formBuilderService: FormBuilderService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private route: ActivatedRoute,
|
||||
protected router: Router) {
|
||||
protected router: Router,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
private modalService: NgbModal,
|
||||
public requestService: RequestService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.initialisePage();
|
||||
}
|
||||
|
||||
initialisePage() {
|
||||
this.subs.push(this.route.params.subscribe((params) => {
|
||||
this.setActiveGroup(params.groupId)
|
||||
}));
|
||||
combineLatest(
|
||||
this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
|
||||
switchMap((group: Group) => {
|
||||
return observableCombineLatest(
|
||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined),
|
||||
this.hasLinkedDSO(group),
|
||||
(isAuthorized: ObservedValueOf<Observable<boolean>>, hasLinkedDSO: ObservedValueOf<Observable<boolean>>) => {
|
||||
return isAuthorized && !hasLinkedDSO;
|
||||
})
|
||||
})
|
||||
);
|
||||
observableCombineLatest(
|
||||
this.translateService.get(`${this.messagePrefix}.groupName`),
|
||||
this.translateService.get(`${this.messagePrefix}.groupDescription`),
|
||||
this.translateService.get(`${this.messagePrefix}.groupDescription`)
|
||||
).subscribe(([groupName, groupDescription]) => {
|
||||
this.groupName = new DynamicInputModel({
|
||||
id: 'groupName',
|
||||
@@ -123,21 +165,26 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
this.formModel = [
|
||||
this.groupName,
|
||||
this.groupDescription
|
||||
this.groupDescription,
|
||||
];
|
||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||
this.subs.push(this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
this.groupBeingEdited = activeGroup;
|
||||
this.formGroup.patchValue({
|
||||
groupName: activeGroup != null ? activeGroup.name : '',
|
||||
groupDescription: activeGroup != null ? activeGroup.firstMetadataValue('dc.description') : '',
|
||||
});
|
||||
if (activeGroup.permanent) {
|
||||
this.formGroup.get('groupName').disable();
|
||||
this.subs.push(
|
||||
observableCombineLatest(
|
||||
this.groupDataService.getActiveGroup(),
|
||||
this.canEdit$
|
||||
).subscribe(([activeGroup, canEdit]) => {
|
||||
if (activeGroup != null) {
|
||||
this.groupBeingEdited = activeGroup;
|
||||
this.formGroup.patchValue({
|
||||
groupName: activeGroup != null ? activeGroup.name : '',
|
||||
groupDescription: activeGroup != null ? activeGroup.firstMetadataValue('dc.description') : '',
|
||||
});
|
||||
if (!canEdit || activeGroup.permanent) {
|
||||
this.formGroup.disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -172,7 +219,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
if (group === null) {
|
||||
this.createNewGroup(values);
|
||||
} else {
|
||||
this.editGroup(group, values);
|
||||
this.editGroup(group);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -193,6 +240,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
if (isNotEmpty(resp.resourceSelfLinks)) {
|
||||
const groupSelfLink = resp.resourceSelfLinks[0];
|
||||
this.setActiveGroupWithLink(groupSelfLink);
|
||||
this.groupDataService.clearGroupsRequests();
|
||||
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLinkWithID(this.groupDataService.getUUIDFromString(groupSelfLink)));
|
||||
}
|
||||
} else {
|
||||
@@ -225,14 +273,32 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* // TODO
|
||||
* @param group
|
||||
* @param values
|
||||
* Edit existing Group based on given values from form and old Group
|
||||
* @param group Group to edit and old values contained within
|
||||
*/
|
||||
editGroup(group: Group, values) {
|
||||
// TODO (backend)
|
||||
console.log('TODO implement editGroup', values);
|
||||
this.notificationsService.error('TODO implement editGroup (not yet implemented in backend) ');
|
||||
editGroup(group: Group) {
|
||||
const editedGroup = Object.assign(new Group(), {
|
||||
id: group.id,
|
||||
metadata: {
|
||||
'dc.description': [
|
||||
{
|
||||
value: (hasValue(this.groupDescription.value) ? this.groupDescription.value : group.firstMetadataValue('dc.description'))
|
||||
}
|
||||
],
|
||||
},
|
||||
name: (hasValue(this.groupName.value) ? this.groupName.value : group.name),
|
||||
_links: group._links,
|
||||
});
|
||||
const response = this.groupDataService.updateGroup(editedGroup);
|
||||
response.pipe(take(1)).subscribe((restResponse: RestResponse) => {
|
||||
if (restResponse.isSuccessful) {
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.edited.success', { name: editedGroup.name }));
|
||||
this.submitForm.emit(editedGroup);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.edited.failure', { name: editedGroup.name }));
|
||||
this.cancelForm.emit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,7 +324,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||
if (activeGroup === null) {
|
||||
this.groupDataService.cancelEditGroup();
|
||||
this.groupDataService.findByHref(groupSelfLink)
|
||||
this.groupDataService.findByHref(groupSelfLink, followLink('subgroups'), followLink('epersons'), followLink('object'))
|
||||
.pipe(
|
||||
getSucceededRemoteData(),
|
||||
getRemoteDataPayload())
|
||||
@@ -269,6 +335,48 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the Group from the Repository. The Group will be the only that this form is showing.
|
||||
* It'll either show a success or error message depending on whether the delete was successful or not.
|
||||
*/
|
||||
delete() {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((group: Group) => {
|
||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||
modalRef.componentInstance.dso = group;
|
||||
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header';
|
||||
modalRef.componentInstance.infoLabel = this.messagePrefix + '.delete-group.modal.info';
|
||||
modalRef.componentInstance.cancelLabel = this.messagePrefix + '.delete-group.modal.cancel';
|
||||
modalRef.componentInstance.confirmLabel = this.messagePrefix + '.delete-group.modal.confirm';
|
||||
modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => {
|
||||
if (confirm) {
|
||||
if (hasValue(group.id)) {
|
||||
this.groupDataService.deleteGroup(group).pipe(take(1))
|
||||
.subscribe(([success, optionalErrorMessage]: [boolean, string]) => {
|
||||
if (success) {
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.deleted.success', { name: group.name }));
|
||||
this.reset();
|
||||
} else {
|
||||
this.notificationsService.error(
|
||||
this.translateService.get(this.messagePrefix + '.notification.deleted.failure.title', { name: group.name }),
|
||||
this.translateService.get(this.messagePrefix + '.notification.deleted.failure.content', { cause: optionalErrorMessage }));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will ensure that the page gets reset and that the cache is cleared
|
||||
*/
|
||||
reset() {
|
||||
this.groupDataService.getBrowseEndpoint().pipe(take(1)).subscribe((href: string) => {
|
||||
this.requestService.removeByHrefSubstring(href);
|
||||
});
|
||||
this.onCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||
*/
|
||||
@@ -277,4 +385,58 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
this.onCancel();
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if group has a linked object (community or collection linked to a workflow group)
|
||||
* @param group
|
||||
*/
|
||||
hasLinkedDSO(group: Group): Observable<boolean> {
|
||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
||||
return this.getLinkedDSO(group).pipe(
|
||||
map((rd: RemoteData<DSpaceObject>) => {
|
||||
if (hasValue(rd) && hasValue(rd.payload)) {
|
||||
return true;
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
catchError(() => observableOf(false)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get group's linked object if it has one (community or collection linked to a workflow group)
|
||||
* @param group
|
||||
*/
|
||||
getLinkedDSO(group: Group): Observable<RemoteData<DSpaceObject>> {
|
||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
||||
if (group.object === undefined) {
|
||||
return this.dSpaceObjectDataService.findByHref(group._links.object.href);
|
||||
}
|
||||
return group.object;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the route to the edit roles tab of the group's linked object (community or collection linked to a workflow group) if it has one
|
||||
* @param group
|
||||
*/
|
||||
getLinkedEditRolesRoute(group: Group): Observable<string> {
|
||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
||||
return this.getLinkedDSO(group).pipe(
|
||||
map((rd: RemoteData<DSpaceObject>) => {
|
||||
if (hasValue(rd) && hasValue(rd.payload)) {
|
||||
const dso = rd.payload
|
||||
switch ((dso as any).type) {
|
||||
case Community.type.value:
|
||||
return getCommunityEditRolesRoute(rd.payload.id);
|
||||
case Collection.type.value:
|
||||
return getCollectionEditRolesRoute(rd.payload.id);
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -30,10 +30,10 @@
|
||||
</form>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(groups | async)?.payload?.totalElements > 0"
|
||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(groups | async)?.payload"
|
||||
[collectionSize]="(groups | async)?.payload?.totalElements"
|
||||
[pageInfoState]="pageInfoState$"
|
||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
@@ -50,21 +50,21 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (groups | async)?.payload?.page">
|
||||
<td>{{group.id}}</td>
|
||||
<td>{{group.name}}</td>
|
||||
<td>{{(getMembers(group) | async)?.payload?.totalElements + (getSubgroups(group) | async)?.payload?.totalElements}}</td>
|
||||
<tr *ngFor="let groupDto of (groupsDto$ | async)?.page">
|
||||
<td>{{groupDto.group.id}}</td>
|
||||
<td>{{groupDto.group.name}}</td>
|
||||
<td>{{(getMembers(groupDto.group) | async)?.payload?.totalElements + (getSubgroups(groupDto.group) | async)?.payload?.totalElements}}</td>
|
||||
<!-- <td>{{getOptionalComColFromName(group.name)}}</td>-->
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<button [routerLink]="groupService.getGroupEditPageRouterLink(group)"
|
||||
<button [routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: group.name} }}">
|
||||
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: groupDto.group.name} }}">
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
<button *ngIf="!group?.permanent" (click)="deleteGroup(group)"
|
||||
class="btn btn-outline-danger btn-sm"
|
||||
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: group.name} }}">
|
||||
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
|
||||
(click)="deleteGroup(groupDto.group)" class="btn btn-outline-danger btn-sm"
|
||||
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: groupDto.group.name} }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -75,7 +75,7 @@
|
||||
</div>
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(groups | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
<div *ngIf="(pageInfoState$ | async)?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{messagePrefix + 'no-items' | translate}}
|
||||
</div>
|
||||
|
||||
|
@@ -6,14 +6,18 @@ import { BrowserModule, By } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { Group } from '../../../core/eperson/models/group.model';
|
||||
import { RouteService } from '../../../core/services/route.service';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock';
|
||||
@@ -30,6 +34,8 @@ describe('GroupRegistryComponent', () => {
|
||||
let fixture: ComponentFixture<GroupsRegistryComponent>;
|
||||
let ePersonDataServiceStub: any;
|
||||
let groupsDataServiceStub: any;
|
||||
let dsoDataServiceStub: any;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
|
||||
let mockGroups;
|
||||
let mockEPeople;
|
||||
@@ -41,11 +47,11 @@ describe('GroupRegistryComponent', () => {
|
||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
switch (href) {
|
||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons':
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, []));
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 0, totalPages: 0, currentPage: 1 }), []));
|
||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/epersons':
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, [EPersonMock]));
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 1, totalPages: 1, currentPage: 1 }), [EPersonMock]));
|
||||
default:
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, []));
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 0, totalPages: 0, currentPage: 1 }), []));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -54,11 +60,11 @@ describe('GroupRegistryComponent', () => {
|
||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
switch (href) {
|
||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/groups':
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, []));
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 0, totalPages: 0, currentPage: 1 }), []));
|
||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/groups':
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, [GroupMock2]));
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 1, totalPages: 1, currentPage: 1 }), [GroupMock2]));
|
||||
default:
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, []));
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 0, totalPages: 0, currentPage: 1 }), []));
|
||||
}
|
||||
},
|
||||
getGroupEditPageRouterLink(group: Group): string {
|
||||
@@ -69,14 +75,22 @@ describe('GroupRegistryComponent', () => {
|
||||
},
|
||||
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
if (query === '') {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allGroups));
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: this.allGroups.length, totalElements: this.allGroups.length, totalPages: 1, currentPage: 1 }), this.allGroups));
|
||||
}
|
||||
const result = this.allGroups.find((group: Group) => {
|
||||
return (group.id.includes(query))
|
||||
});
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: [result].length, totalElements: [result].length, totalPages: 1, currentPage: 1 }), [result]));
|
||||
}
|
||||
};
|
||||
dsoDataServiceStub = {
|
||||
findByHref(href: string): Observable<RemoteData<DSpaceObject>> {
|
||||
return createSuccessfulRemoteDataObject$(undefined);
|
||||
}
|
||||
}
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true)
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
@@ -90,9 +104,12 @@ describe('GroupRegistryComponent', () => {
|
||||
providers: [GroupsRegistryComponent,
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
{ provide: GroupDataService, useValue: groupsDataServiceStub },
|
||||
{ provide: DSpaceObjectDataService, useValue: dsoDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{ provide: Router, useValue: new RouterMock() },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])}
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
@@ -1,16 +1,26 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription, Observable, of as observableOf } from 'rxjs';
|
||||
import { filter } from 'rxjs/internal/operators/filter';
|
||||
import { ObservedValueOf } from 'rxjs/internal/types';
|
||||
import { catchError, map, switchMap, take } from 'rxjs/operators';
|
||||
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { GroupDtoModel } from '../../../core/eperson/models/group-dto.model';
|
||||
import { Group } from '../../../core/eperson/models/group.model';
|
||||
import { RouteService } from '../../../core/services/route.service';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
@@ -23,7 +33,7 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio
|
||||
* A component used for managing all existing groups within the repository.
|
||||
* The admin can create, edit or delete groups here.
|
||||
*/
|
||||
export class GroupsRegistryComponent implements OnInit {
|
||||
export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
|
||||
messagePrefix = 'admin.access-control.groups.';
|
||||
|
||||
@@ -37,9 +47,19 @@ export class GroupsRegistryComponent implements OnInit {
|
||||
});
|
||||
|
||||
/**
|
||||
* A list of all the current groups within the repository or the result of the search
|
||||
* A list of all the current Groups within the repository or the result of the search
|
||||
*/
|
||||
groups: Observable<RemoteData<PaginatedList<Group>>>;
|
||||
groups$: BehaviorSubject<RemoteData<PaginatedList<Group>>> = new BehaviorSubject<RemoteData<PaginatedList<Group>>>({} as any);
|
||||
/**
|
||||
* A BehaviorSubject with the list of GroupDtoModel objects made from the Groups in the repository or
|
||||
* as the result of the search
|
||||
*/
|
||||
groupsDto$: BehaviorSubject<PaginatedList<GroupDtoModel>> = new BehaviorSubject<PaginatedList<GroupDtoModel>>({} as any);
|
||||
|
||||
/**
|
||||
* An observable for the pageInfo, needed to pass to the pagination component
|
||||
*/
|
||||
pageInfoState$: BehaviorSubject<PageInfo> = new BehaviorSubject<PageInfo>(undefined);
|
||||
|
||||
// The search form
|
||||
searchForm;
|
||||
@@ -47,13 +67,21 @@ export class GroupsRegistryComponent implements OnInit {
|
||||
// Current search in groups registry
|
||||
currentSearchQuery: string;
|
||||
|
||||
/**
|
||||
* List of subscriptions
|
||||
*/
|
||||
subs: Subscription[] = [];
|
||||
|
||||
constructor(public groupService: GroupDataService,
|
||||
private ePersonDataService: EPersonDataService,
|
||||
private dSpaceObjectDataService: DSpaceObjectDataService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private formBuilder: FormBuilder,
|
||||
protected routeService: RouteService,
|
||||
private router: Router) {
|
||||
private router: Router,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
public requestService: RequestService) {
|
||||
this.currentSearchQuery = '';
|
||||
this.searchForm = this.formBuilder.group(({
|
||||
query: this.currentSearchQuery,
|
||||
@@ -84,37 +112,69 @@ export class GroupsRegistryComponent implements OnInit {
|
||||
this.currentSearchQuery = query;
|
||||
this.config.currentPage = 1;
|
||||
}
|
||||
this.groups = this.groupService.searchGroups(this.currentSearchQuery.trim(), {
|
||||
this.subs.push(this.groupService.searchGroups(this.currentSearchQuery.trim(), {
|
||||
currentPage: this.config.currentPage,
|
||||
elementsPerPage: this.config.pageSize
|
||||
});
|
||||
}).subscribe((groupsRD: RemoteData<PaginatedList<Group>>) => {
|
||||
this.groups$.next(groupsRD);
|
||||
this.pageInfoState$.next(groupsRD.payload.pageInfo);
|
||||
}
|
||||
));
|
||||
|
||||
this.subs.push(this.groups$.pipe(
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
switchMap((groups: PaginatedList<Group>) => {
|
||||
return observableCombineLatest(...groups.page.map((group: Group) => {
|
||||
return observableCombineLatest(
|
||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined),
|
||||
this.hasLinkedDSO(group),
|
||||
(isAuthorized: ObservedValueOf<Observable<boolean>>, hasLinkedDSO: ObservedValueOf<Observable<boolean>>) => {
|
||||
const groupDtoModel: GroupDtoModel = new GroupDtoModel();
|
||||
groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO;
|
||||
groupDtoModel.group = group;
|
||||
return groupDtoModel;
|
||||
}
|
||||
)
|
||||
})).pipe(map((dtos: GroupDtoModel[]) => {
|
||||
return new PaginatedList(groups.pageInfo, dtos);
|
||||
}))
|
||||
})).subscribe((value: PaginatedList<GroupDtoModel>) => {
|
||||
this.groupsDto$.next(value);
|
||||
this.pageInfoState$.next(value.pageInfo);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Group
|
||||
*/
|
||||
deleteGroup(group: Group) {
|
||||
// TODO (backend)
|
||||
console.log('TODO implement editGroup', group);
|
||||
this.notificationsService.error('TODO implement deleteGroup (not yet implemented in backend)');
|
||||
if (hasValue(group.id)) {
|
||||
this.groupService.deleteGroup(group).pipe(take(1)).subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.name }));
|
||||
this.forceUpdateGroup();
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + 'notification.deleted.failure', { name: group.name }));
|
||||
}
|
||||
})
|
||||
this.groupService.deleteGroup(group).pipe(take(1))
|
||||
.subscribe(([success, optionalErrorMessage]: [boolean, string]) => {
|
||||
if (success) {
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.name }));
|
||||
this.reset();
|
||||
} else {
|
||||
this.notificationsService.error(
|
||||
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.name }),
|
||||
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.content', { cause: optionalErrorMessage }));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-update the list of groups by first clearing the cache related to groups, then performing a new REST call
|
||||
* This method will ensure that the page gets reset and that the cache is cleared
|
||||
*/
|
||||
public forceUpdateGroup() {
|
||||
this.groupService.clearGroupsRequests();
|
||||
this.search({ query: this.currentSearchQuery })
|
||||
reset() {
|
||||
this.groupService.getBrowseEndpoint().pipe(
|
||||
switchMap((href) => this.requestService.removeByHrefSubstring(href)),
|
||||
filter((isCached) => isCached),
|
||||
take(1)
|
||||
).subscribe(() => {
|
||||
this.cleanupSubscribes();
|
||||
this.search({ query: this.currentSearchQuery });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,6 +193,23 @@ export class GroupsRegistryComponent implements OnInit {
|
||||
return this.groupService.findAllByHref(group._links.subgroups.href);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if group has a linked object (community or collection linked to a workflow group)
|
||||
* @param group
|
||||
*/
|
||||
hasLinkedDSO(group: Group): Observable<boolean> {
|
||||
return this.dSpaceObjectDataService.findByHref(group._links.object.href).pipe(
|
||||
map((rd: RemoteData<DSpaceObject>) => {
|
||||
if (hasValue(rd) && hasValue(rd.payload)) {
|
||||
return true;
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}),
|
||||
catchError(() => observableOf(false)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all input-fields to be empty and search all search
|
||||
*/
|
||||
@@ -151,4 +228,15 @@ export class GroupsRegistryComponent implements OnInit {
|
||||
getOptionalComColFromName(groupName: string): string {
|
||||
return this.groupService.getUUIDFromString(groupName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsub all subscriptions
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.cleanupSubscribes();
|
||||
}
|
||||
|
||||
cleanupSubscribes() {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import { of as observableOf } from 'rxjs';
|
||||
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -17,8 +18,9 @@ import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||
export class CollectionPageAdministratorGuard extends DsoPageFeatureGuard<Collection> {
|
||||
constructor(protected resolver: CollectionPageResolver,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router) {
|
||||
super(resolver, authorizationService, router);
|
||||
protected router: Router,
|
||||
protected authService: AuthService) {
|
||||
super(resolver, authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -20,6 +20,11 @@ export function getCollectionCreateRoute() {
|
||||
return new URLCombiner(getCollectionModuleRoute(), COLLECTION_CREATE_PATH).toString()
|
||||
}
|
||||
|
||||
export function getCollectionEditRolesRoute(id) {
|
||||
return new URLCombiner(getCollectionPageRoute(id), COLLECTION_EDIT_PATH, COLLECTION_EDIT_ROLES_PATH).toString()
|
||||
}
|
||||
|
||||
export const COLLECTION_CREATE_PATH = 'create';
|
||||
export const COLLECTION_EDIT_PATH = 'edit';
|
||||
export const COLLECTION_EDIT_ROLES_PATH = 'roles';
|
||||
export const ITEMTEMPLATE_PATH = 'itemtemplate';
|
||||
|
@@ -17,13 +17,14 @@ import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
|
||||
import { Item } from '../core/shared/item.model';
|
||||
import {
|
||||
getSucceededRemoteData,
|
||||
redirectOn404Or401,
|
||||
redirectOn4xx,
|
||||
toDSpaceObjectListRD
|
||||
} from '../core/shared/operators';
|
||||
|
||||
import { fadeIn, fadeInOut } from '../shared/animations/fade';
|
||||
import { hasNoValue, hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-collection-page',
|
||||
@@ -51,7 +52,8 @@ export class CollectionPageComponent implements OnInit {
|
||||
private searchService: SearchService,
|
||||
private metadata: MetadataService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
private router: Router,
|
||||
private authService: AuthService,
|
||||
) {
|
||||
this.paginationConfig = new PaginationComponentOptions();
|
||||
this.paginationConfig.id = 'collection-page-pagination';
|
||||
@@ -63,7 +65,7 @@ export class CollectionPageComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.collectionRD$ = this.route.data.pipe(
|
||||
map((data) => data.dso as RemoteData<Collection>),
|
||||
redirectOn404Or401(this.router),
|
||||
redirectOn4xx(this.router, this.authService),
|
||||
take(1)
|
||||
);
|
||||
this.logoRD$ = this.collectionRD$.pipe(
|
||||
|
@@ -17,7 +17,7 @@ describe('CollectionCurateComponent', () => {
|
||||
let dsoNameService;
|
||||
|
||||
const collection = Object.assign(new Collection(), {
|
||||
handle: '123456789/1', metadata: {'dc.title': ['Collection Name']}
|
||||
metadata: {'dc.title': ['Collection Name'], 'dc.identifier.uri': [ { value: '123456789/1'}]}
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
|
@@ -7,6 +7,7 @@ import { of as observableOf } from 'rxjs';
|
||||
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -17,8 +18,9 @@ import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||
export class CommunityPageAdministratorGuard extends DsoPageFeatureGuard<Community> {
|
||||
constructor(protected resolver: CommunityPageResolver,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router) {
|
||||
super(resolver, authorizationService, router);
|
||||
protected router: Router,
|
||||
protected authService: AuthService) {
|
||||
super(resolver, authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { getCollectionPageRoute } from '../+collection-page/collection-page-routing-paths';
|
||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
|
||||
export const COMMUNITY_PARENT_PARAMETER = 'parent';
|
||||
@@ -20,5 +21,10 @@ export function getCommunityCreateRoute() {
|
||||
return new URLCombiner(getCommunityModuleRoute(), COMMUNITY_CREATE_PATH).toString()
|
||||
}
|
||||
|
||||
export function getCommunityEditRolesRoute(id) {
|
||||
return new URLCombiner(getCollectionPageRoute(id), COMMUNITY_EDIT_PATH, COMMUNITY_EDIT_ROLES_PATH).toString()
|
||||
}
|
||||
|
||||
export const COMMUNITY_CREATE_PATH = 'create';
|
||||
export const COMMUNITY_EDIT_PATH = 'edit';
|
||||
export const COMMUNITY_EDIT_ROLES_PATH = 'roles';
|
||||
|
@@ -13,7 +13,8 @@ import { MetadataService } from '../core/metadata/metadata.service';
|
||||
|
||||
import { fadeInOut } from '../shared/animations/fade';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { redirectOn404Or401 } from '../core/shared/operators';
|
||||
import { redirectOn4xx } from '../core/shared/operators';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-community-page',
|
||||
@@ -39,7 +40,8 @@ export class CommunityPageComponent implements OnInit {
|
||||
private communityDataService: CommunityDataService,
|
||||
private metadata: MetadataService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
private router: Router,
|
||||
private authService: AuthService,
|
||||
) {
|
||||
|
||||
}
|
||||
@@ -47,7 +49,7 @@ export class CommunityPageComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.communityRD$ = this.route.data.pipe(
|
||||
map((data) => data.dso as RemoteData<Community>),
|
||||
redirectOn404Or401(this.router)
|
||||
redirectOn4xx(this.router, this.authService)
|
||||
);
|
||||
this.logoRD$ = this.communityRD$.pipe(
|
||||
map((rd: RemoteData<Community>) => rd.payload),
|
||||
|
@@ -17,7 +17,7 @@ describe('CommunityCurateComponent', () => {
|
||||
let dsoNameService;
|
||||
|
||||
const community = Object.assign(new Community(), {
|
||||
handle: '123456789/1', metadata: {'dc.title': ['Community Name']}
|
||||
metadata: {'dc.title': ['Community Name'], 'dc.identifier.uri': [ { value: '123456789/1'}]}
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<span>{{metadata?.key?.split('.').join('.​')}}</span>
|
||||
</div>
|
||||
<div *ngIf="(editable | async)" class="field-container">
|
||||
<ds-filter-input-suggestions [suggestions]="(metadataFieldSuggestions | async)"
|
||||
<ds-validation-suggestions [suggestions]="(metadataFieldSuggestions | async)"
|
||||
[(ngModel)]="metadata.key"
|
||||
[url]="this.url"
|
||||
[metadata]="this.metadata"
|
||||
@@ -17,7 +17,7 @@
|
||||
[valid]="(valid | async) !== false"
|
||||
dsAutoFocus autoFocusSelector=".suggestion_input"
|
||||
[ngModelOptions]="{standalone: true}"
|
||||
></ds-filter-input-suggestions>
|
||||
></ds-validation-suggestions>
|
||||
</div>
|
||||
<small class="text-danger"
|
||||
*ngIf="(valid | async) === false">{{"item.edit.metadata.metadatafield.invalid" | translate}}</small>
|
||||
|
@@ -20,9 +20,9 @@ import {
|
||||
} from '../../../../shared/remote-data.utils';
|
||||
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||
import { EditInPlaceFieldComponent } from './edit-in-place-field.component';
|
||||
import { FilterInputSuggestionsComponent } from '../../../../shared/input-suggestions/filter-suggestions/filter-input-suggestions.component';
|
||||
import { MockComponent, MockDirective } from 'ng-mocks';
|
||||
import { DebounceDirective } from '../../../../shared/utils/debounce.directive';
|
||||
import { ValidationSuggestionsComponent } from '../../../../shared/input-suggestions/validation-suggestions/validation-suggestions.component';
|
||||
|
||||
let comp: EditInPlaceFieldComponent;
|
||||
let fixture: ComponentFixture<EditInPlaceFieldComponent>;
|
||||
@@ -88,7 +88,7 @@ describe('EditInPlaceFieldComponent', () => {
|
||||
declarations: [
|
||||
EditInPlaceFieldComponent,
|
||||
MockDirective(DebounceDirective),
|
||||
MockComponent(FilterInputSuggestionsComponent)
|
||||
MockComponent(ValidationSuggestionsComponent)
|
||||
],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: metadataFieldService },
|
||||
|
@@ -7,6 +7,7 @@ import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/ro
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -17,8 +18,9 @@ import { of as observableOf } from 'rxjs';
|
||||
export class ItemPageReinstateGuard extends DsoPageFeatureGuard<Item> {
|
||||
constructor(protected resolver: ItemPageResolver,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router) {
|
||||
super(resolver, authorizationService, router);
|
||||
protected router: Router,
|
||||
protected authService: AuthService) {
|
||||
super(resolver, authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -7,6 +7,7 @@ import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/ro
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -17,8 +18,9 @@ import { of as observableOf } from 'rxjs';
|
||||
export class ItemPageWithdrawGuard extends DsoPageFeatureGuard<Item> {
|
||||
constructor(protected resolver: ItemPageResolver,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router) {
|
||||
super(resolver, authorizationService, router);
|
||||
protected router: Router,
|
||||
protected authService: AuthService) {
|
||||
super(resolver, authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -20,6 +20,7 @@ import {
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
@@ -45,7 +46,14 @@ describe('FullItemPageComponent', () => {
|
||||
let comp: FullItemPageComponent;
|
||||
let fixture: ComponentFixture<FullItemPageComponent>;
|
||||
|
||||
let authService: AuthService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true),
|
||||
setRedirectUrl: {}
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
@@ -57,7 +65,8 @@ describe('FullItemPageComponent', () => {
|
||||
providers: [
|
||||
{provide: ActivatedRoute, useValue: routeStub},
|
||||
{provide: ItemDataService, useValue: {}},
|
||||
{provide: MetadataService, useValue: metadataServiceStub}
|
||||
{provide: MetadataService, useValue: metadataServiceStub},
|
||||
{ provide: AuthService, useValue: authService },
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -15,6 +15,7 @@ import { MetadataService } from '../../core/metadata/metadata.service';
|
||||
|
||||
import { fadeInOut } from '../../shared/animations/fade';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
/**
|
||||
* This component renders a simple item page.
|
||||
@@ -35,8 +36,8 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit {
|
||||
|
||||
metadata$: Observable<MetadataMap>;
|
||||
|
||||
constructor(route: ActivatedRoute, router: Router, items: ItemDataService, metadataService: MetadataService) {
|
||||
super(route, router, items, metadataService);
|
||||
constructor(route: ActivatedRoute, router: Router, items: ItemDataService, metadataService: MetadataService, authService: AuthService) {
|
||||
super(route, router, items, metadataService, authService);
|
||||
}
|
||||
|
||||
/*** AoT inheritance fix, will hopefully be resolved in the near future **/
|
||||
|
@@ -7,6 +7,7 @@ import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -17,8 +18,9 @@ import { of as observableOf } from 'rxjs';
|
||||
export class ItemPageAdministratorGuard extends DsoPageFeatureGuard<Item> {
|
||||
constructor(protected resolver: ItemPageResolver,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router) {
|
||||
super(resolver, authorizationService, router);
|
||||
protected router: Router,
|
||||
protected authService: AuthService) {
|
||||
super(resolver, authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -19,6 +19,7 @@ import {
|
||||
createFailedRemoteDataObject$, createPendingRemoteDataObject$, createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||
@@ -29,6 +30,7 @@ const mockItem: Item = Object.assign(new Item(), {
|
||||
describe('ItemPageComponent', () => {
|
||||
let comp: ItemPageComponent;
|
||||
let fixture: ComponentFixture<ItemPageComponent>;
|
||||
let authService: AuthService;
|
||||
|
||||
const mockMetadataService = {
|
||||
/* tslint:disable:no-empty */
|
||||
@@ -40,6 +42,11 @@ describe('ItemPageComponent', () => {
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true),
|
||||
setRedirectUrl: {}
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
@@ -52,7 +59,8 @@ describe('ItemPageComponent', () => {
|
||||
{provide: ActivatedRoute, useValue: mockRoute},
|
||||
{provide: ItemDataService, useValue: {}},
|
||||
{provide: MetadataService, useValue: mockMetadataService},
|
||||
{provide: Router, useValue: {}}
|
||||
{provide: Router, useValue: {}},
|
||||
{ provide: AuthService, useValue: authService },
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
|
@@ -11,8 +11,9 @@ import { Item } from '../../core/shared/item.model';
|
||||
import { MetadataService } from '../../core/metadata/metadata.service';
|
||||
|
||||
import { fadeInOut } from '../../shared/animations/fade';
|
||||
import { redirectOn404Or401 } from '../../core/shared/operators';
|
||||
import { redirectOn4xx } from '../../core/shared/operators';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
/**
|
||||
* This component renders a simple item page.
|
||||
@@ -48,6 +49,7 @@ export class ItemPageComponent implements OnInit {
|
||||
private router: Router,
|
||||
private items: ItemDataService,
|
||||
private metadataService: MetadataService,
|
||||
private authService: AuthService,
|
||||
) { }
|
||||
|
||||
/**
|
||||
@@ -56,7 +58,7 @@ export class ItemPageComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.itemRD$ = this.route.data.pipe(
|
||||
map((data) => data.dso as RemoteData<Item>),
|
||||
redirectOn404Or401(this.router)
|
||||
redirectOn4xx(this.router, this.authService)
|
||||
);
|
||||
this.metadataService.processRemoteData(this.itemRD$);
|
||||
}
|
||||
|
@@ -55,10 +55,10 @@ export function getDSORoute(dso: DSpaceObject): string {
|
||||
}
|
||||
}
|
||||
|
||||
export const UNAUTHORIZED_PATH = '401';
|
||||
export const FORBIDDEN_PATH = '403';
|
||||
|
||||
export function getUnauthorizedRoute() {
|
||||
return `/${UNAUTHORIZED_PATH}`;
|
||||
export function getForbiddenRoute() {
|
||||
return `/${FORBIDDEN_PATH}`;
|
||||
}
|
||||
|
||||
export const PAGE_NOT_FOUND_PATH = '404';
|
||||
|
@@ -5,16 +5,15 @@ import { AuthBlockingGuard } from './core/auth/auth-blocking.guard';
|
||||
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
||||
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
|
||||
import { SiteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
|
||||
import {
|
||||
UNAUTHORIZED_PATH,
|
||||
WORKFLOW_ITEM_MODULE_PATH,
|
||||
FORGOT_PASSWORD_PATH,
|
||||
REGISTER_PATH,
|
||||
PROFILE_MODULE_PATH,
|
||||
ADMIN_MODULE_PATH,
|
||||
BITSTREAM_MODULE_PATH,
|
||||
INFO_MODULE_PATH
|
||||
INFO_MODULE_PATH,
|
||||
FORBIDDEN_PATH,
|
||||
} from './app-routing-paths';
|
||||
import { COLLECTION_MODULE_PATH } from './+collection-page/collection-page-routing-paths';
|
||||
import { COMMUNITY_MODULE_PATH } from './+community-page/community-page-routing-paths';
|
||||
@@ -22,6 +21,7 @@ import { ITEM_MODULE_PATH } from './+item-page/item-page-routing-paths';
|
||||
import { ReloadGuard } from './core/reload/reload.guard';
|
||||
import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.guard';
|
||||
import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard';
|
||||
import { ForbiddenComponent } from './forbidden/forbidden.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -68,7 +68,7 @@ import { SiteRegisterGuard } from './core/data/feature-authorization/feature-aut
|
||||
},
|
||||
{ path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard] },
|
||||
{ path: INFO_MODULE_PATH, loadChildren: './info/info.module#InfoModule' },
|
||||
{ path: UNAUTHORIZED_PATH, component: UnauthorizedComponent },
|
||||
{ path: FORBIDDEN_PATH, component: ForbiddenComponent },
|
||||
{
|
||||
path: 'statistics',
|
||||
loadChildren: './statistics-page/statistics-page-routing.module#StatisticsPageRoutingModule',
|
||||
|
@@ -41,7 +41,7 @@ import { SharedModule } from './shared/shared.module';
|
||||
import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component';
|
||||
import { environment } from '../environments/environment';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
|
||||
import { ForbiddenComponent } from './forbidden/forbidden.component';
|
||||
|
||||
export function getBase() {
|
||||
return environment.ui.nameSpace;
|
||||
@@ -116,6 +116,8 @@ const DECLARATIONS = [
|
||||
NotificationComponent,
|
||||
NotificationsBoardComponent,
|
||||
SearchNavbarComponent,
|
||||
BreadcrumbsComponent,
|
||||
ForbiddenComponent,
|
||||
];
|
||||
|
||||
const EXPORTS = [
|
||||
@@ -133,8 +135,6 @@ const EXPORTS = [
|
||||
],
|
||||
declarations: [
|
||||
...DECLARATIONS,
|
||||
BreadcrumbsComponent,
|
||||
UnauthorizedComponent,
|
||||
],
|
||||
exports: [
|
||||
...EXPORTS
|
||||
|
@@ -453,7 +453,7 @@ export class AuthService {
|
||||
* Clear redirect url
|
||||
*/
|
||||
clearRedirectUrl() {
|
||||
this.store.dispatch(new SetRedirectUrlAction(''));
|
||||
this.store.dispatch(new SetRedirectUrlAction(undefined));
|
||||
this.storage.remove(REDIRECT_COOKIE);
|
||||
}
|
||||
|
||||
|
@@ -58,4 +58,8 @@ export class DSpaceObjectDataService {
|
||||
findById(uuid: string): Observable<RemoteData<DSpaceObject>> {
|
||||
return this.dataService.findById(uuid);
|
||||
}
|
||||
|
||||
findByHref(href: string): Observable<RemoteData<DSpaceObject>> {
|
||||
return this.dataService.findByHref(href);
|
||||
}
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteData } from '../remote-data';
|
||||
import { PaginatedList } from '../paginated-list';
|
||||
import { find, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { catchError, find, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { RequestParam } from '../../cache/models/request-param.model';
|
||||
import { AuthorizationSearchParams } from './authorization-search-params';
|
||||
@@ -71,6 +71,7 @@ export class AuthorizationDataService extends DataService<Authorization> {
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
catchError(() => observableOf(false)),
|
||||
oneAuthorizationMatchesFeature(featureId)
|
||||
);
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||
import { DsoPageFeatureGuard } from './dso-page-feature.guard';
|
||||
import { FeatureID } from '../feature-id';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { AuthService } from '../../../auth/auth.service';
|
||||
|
||||
/**
|
||||
* Test implementation of abstract class DsoPageAdministratorGuard
|
||||
@@ -15,8 +16,9 @@ class DsoPageFeatureGuardImpl extends DsoPageFeatureGuard<any> {
|
||||
constructor(protected resolver: Resolve<RemoteData<any>>,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router,
|
||||
protected authService: AuthService,
|
||||
protected featureID: FeatureID) {
|
||||
super(resolver, authorizationService, router);
|
||||
super(resolver, authorizationService, router, authService);
|
||||
}
|
||||
|
||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||
@@ -28,6 +30,7 @@ describe('DsoPageAdministratorGuard', () => {
|
||||
let guard: DsoPageFeatureGuard<any>;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let router: Router;
|
||||
let authService: AuthService;
|
||||
let resolver: Resolve<RemoteData<any>>;
|
||||
let object: DSpaceObject;
|
||||
|
||||
@@ -45,7 +48,10 @@ describe('DsoPageAdministratorGuard', () => {
|
||||
resolver = jasmine.createSpyObj('resolver', {
|
||||
resolve: createSuccessfulRemoteDataObject$(object)
|
||||
});
|
||||
guard = new DsoPageFeatureGuardImpl(resolver, authorizationService, router, undefined);
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true)
|
||||
});
|
||||
guard = new DsoPageFeatureGuardImpl(resolver, authorizationService, router, authService, undefined);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
@@ -6,6 +6,7 @@ import { getAllSucceededRemoteDataPayload } from '../../../shared/operators';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
||||
import { AuthService } from '../../../auth/auth.service';
|
||||
|
||||
/**
|
||||
* Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature
|
||||
@@ -14,8 +15,9 @@ import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
||||
export abstract class DsoPageFeatureGuard<T extends DSpaceObject> extends FeatureAuthorizationGuard {
|
||||
constructor(protected resolver: Resolve<RemoteData<T>>,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router) {
|
||||
super(authorizationService, router);
|
||||
protected router: Router,
|
||||
protected authService: AuthService) {
|
||||
super(authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -4,6 +4,7 @@ import { FeatureID } from '../feature-id';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { AuthService } from '../../../auth/auth.service';
|
||||
|
||||
/**
|
||||
* Test implementation of abstract class FeatureAuthorizationGuard
|
||||
@@ -12,10 +13,11 @@ import { Observable } from 'rxjs/internal/Observable';
|
||||
class FeatureAuthorizationGuardImpl extends FeatureAuthorizationGuard {
|
||||
constructor(protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router,
|
||||
protected authService: AuthService,
|
||||
protected featureId: FeatureID,
|
||||
protected objectUrl: string,
|
||||
protected ePersonUuid: string) {
|
||||
super(authorizationService, router);
|
||||
super(authorizationService, router, authService);
|
||||
}
|
||||
|
||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||
@@ -35,6 +37,7 @@ describe('FeatureAuthorizationGuard', () => {
|
||||
let guard: FeatureAuthorizationGuard;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let router: Router;
|
||||
let authService: AuthService;
|
||||
|
||||
let featureId: FeatureID;
|
||||
let objectUrl: string;
|
||||
@@ -51,7 +54,10 @@ describe('FeatureAuthorizationGuard', () => {
|
||||
router = jasmine.createSpyObj('router', {
|
||||
parseUrl: {}
|
||||
});
|
||||
guard = new FeatureAuthorizationGuardImpl(authorizationService, router, featureId, objectUrl, ePersonUuid);
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true)
|
||||
});
|
||||
guard = new FeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -60,7 +66,7 @@ describe('FeatureAuthorizationGuard', () => {
|
||||
|
||||
describe('canActivate', () => {
|
||||
it('should call authorizationService.isAuthenticated with the appropriate arguments', () => {
|
||||
guard.canActivate(undefined, undefined).subscribe();
|
||||
guard.canActivate(undefined, { url: 'current-url' } as any).subscribe();
|
||||
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(featureId, objectUrl, ePersonUuid);
|
||||
});
|
||||
});
|
||||
|
@@ -8,9 +8,10 @@ import {
|
||||
import { AuthorizationDataService } from '../authorization-data.service';
|
||||
import { FeatureID } from '../feature-id';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { returnUnauthorizedUrlTreeOnFalse } from '../../../shared/operators';
|
||||
import { returnForbiddenUrlTreeOrLoginOnFalse } from '../../../shared/operators';
|
||||
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { AuthService } from '../../../auth/auth.service';
|
||||
|
||||
/**
|
||||
* Abstract Guard for preventing unauthorized activating and loading of routes when a user
|
||||
@@ -19,7 +20,8 @@ import { switchMap } from 'rxjs/operators';
|
||||
*/
|
||||
export abstract class FeatureAuthorizationGuard implements CanActivate {
|
||||
constructor(protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router) {
|
||||
protected router: Router,
|
||||
protected authService: AuthService) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,7 +31,7 @@ export abstract class FeatureAuthorizationGuard implements CanActivate {
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
||||
return observableCombineLatest(this.getFeatureID(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe(
|
||||
switchMap(([featureID, objectUrl, ePersonUuid]) => this.authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid)),
|
||||
returnUnauthorizedUrlTreeOnFalse(this.router)
|
||||
returnForbiddenUrlTreeOrLoginOnFalse(this.router, this.authService, state.url)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -5,6 +5,7 @@ import { AuthorizationDataService } from '../authorization-data.service';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { AuthService } from '../../../auth/auth.service';
|
||||
|
||||
/**
|
||||
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have administrator
|
||||
@@ -14,8 +15,8 @@ import { Observable } from 'rxjs/internal/Observable';
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SiteAdministratorGuard extends FeatureAuthorizationGuard {
|
||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router) {
|
||||
super(authorizationService, router);
|
||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
||||
super(authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -5,6 +5,7 @@ import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/ro
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { FeatureID } from '../feature-id';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { AuthService } from '../../../auth/auth.service';
|
||||
|
||||
/**
|
||||
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have registration
|
||||
@@ -14,8 +15,8 @@ import { of as observableOf } from 'rxjs';
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SiteRegisterGuard extends FeatureAuthorizationGuard {
|
||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router) {
|
||||
super(authorizationService, router);
|
||||
constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) {
|
||||
super(authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -303,7 +303,7 @@ describe('EPersonDataService', () => {
|
||||
it('should sent a patch request with an uuid, token and new password to the epersons endpoint', () => {
|
||||
service.patchPasswordWithToken('test-uuid', 'test-token','test-password');
|
||||
|
||||
const operation = Object.assign({ op: 'replace', path: '/password', value: 'test-password' });
|
||||
const operation = Object.assign({ op: 'add', path: '/password', value: 'test-password' });
|
||||
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/test-uuid?token=test-token', [operation]);
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
|
@@ -280,7 +280,7 @@ export class EPersonDataService extends DataService<EPerson> {
|
||||
patchPasswordWithToken(uuid: string, token: string, password: string): Observable<RestResponse> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
const operation = Object.assign({ op: 'replace', path: '/password', value: password });
|
||||
const operation = Object.assign({ op: 'add', path: '/password', value: password });
|
||||
|
||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||
map((endpoint: string) => this.getIDHref(endpoint, uuid)),
|
||||
|
@@ -2,6 +2,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { createSelector, select, Store } from '@ngrx/store';
|
||||
import { Operation } from 'fast-json-patch/lib/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, map, take, tap } from 'rxjs/operators';
|
||||
import {
|
||||
@@ -16,17 +17,24 @@ import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { RequestParam } from '../cache/models/request-param.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { ErrorResponse, RestResponse } from '../cache/response.models';
|
||||
import { DataService } from '../data/data.service';
|
||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { CreateRequest, DeleteRequest, FindListOptions, FindListRequest, PostRequest } from '../data/request.models';
|
||||
import {
|
||||
CreateRequest,
|
||||
DeleteRequest,
|
||||
FindListOptions,
|
||||
FindListRequest,
|
||||
PatchRequest,
|
||||
PostRequest
|
||||
} from '../data/request.models';
|
||||
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { getResponseFromEntry } from '../shared/operators';
|
||||
import { getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators';
|
||||
import { EPerson } from './models/eperson.model';
|
||||
import { Group } from './models/group.model';
|
||||
import { dataService } from '../cache/builders/build-decorators';
|
||||
@@ -125,33 +133,51 @@ export class GroupDataService extends DataService<Group> {
|
||||
|
||||
/**
|
||||
* Method to delete a group
|
||||
* @param id The group id to delete
|
||||
* @param group The group to delete
|
||||
*/
|
||||
public deleteGroup(group: Group): Observable<boolean> {
|
||||
return this.delete(group.id).pipe(map((response: RestResponse) => response.isSuccessful));
|
||||
public deleteGroup(group: Group): Observable<[boolean, string]> {
|
||||
return this.delete(group.id).pipe(map((response: RestResponse) => {
|
||||
const errorMessage = response.isSuccessful === false ? (response as ErrorResponse).errorMessage : undefined;
|
||||
return [response.isSuccessful, errorMessage];
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or Update a group
|
||||
* If the group contains an id, it is assumed the eperson already exists and is updated instead
|
||||
* @param group The group to create or update
|
||||
* Add a new patch to the object cache
|
||||
* The patch is derived from the differences between the given object and its version in the object cache
|
||||
* @param group The group with changes
|
||||
*/
|
||||
public createOrUpdateGroup(group: Group): Observable<RemoteData<Group>> {
|
||||
const isUpdate = hasValue(group.id);
|
||||
if (isUpdate) {
|
||||
return this.updateGroup(group);
|
||||
} else {
|
||||
return this.create(group, null);
|
||||
updateGroup(group: Group): Observable<RestResponse> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
const oldVersion$ = this.findByHref(group._links.self.href);
|
||||
oldVersion$.pipe(
|
||||
getSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((oldGroup: Group) => {
|
||||
const operations = this.generateOperations(oldGroup, group);
|
||||
const patchRequest = new PatchRequest(requestId, group._links.self.href, operations);
|
||||
return this.requestService.configure(patchRequest);
|
||||
}),
|
||||
take(1)
|
||||
).subscribe();
|
||||
|
||||
return this.fetchResponse(requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata operations are generated by the difference between old and new Group
|
||||
* Custom replace operation for the other group Name value
|
||||
* @param oldGroup
|
||||
* @param newGroup
|
||||
*/
|
||||
private generateOperations(oldGroup: Group, newGroup: Group): Operation[] {
|
||||
let operations = this.comparator.diff(oldGroup, newGroup).filter((operation: Operation) => operation.op === 'replace');
|
||||
if (hasValue(oldGroup.name) && oldGroup.name !== newGroup.name) {
|
||||
operations = [...operations, {
|
||||
op: 'replace', path: '/name', value: newGroup.name
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* // TODO
|
||||
* @param {DSpaceObject} ePerson The given object
|
||||
*/
|
||||
updateGroup(group: Group): Observable<RemoteData<Group>> {
|
||||
// TODO
|
||||
return null;
|
||||
return operations;
|
||||
}
|
||||
|
||||
/**
|
||||
|
17
src/app/core/eperson/models/group-dto.model.ts
Normal file
17
src/app/core/eperson/models/group-dto.model.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Group } from './group.model';
|
||||
|
||||
/**
|
||||
* This class serves as a Data Transfer Model that contains the Group and whether or not it's able to be deleted
|
||||
*/
|
||||
export class GroupDtoModel {
|
||||
|
||||
/**
|
||||
* The Group linked to this object
|
||||
*/
|
||||
public group: Group;
|
||||
/**
|
||||
* Whether or not the linked Group is able to be deleted
|
||||
*/
|
||||
public ableToDelete: boolean;
|
||||
|
||||
}
|
@@ -5,6 +5,7 @@ import { PaginatedList } from '../../data/paginated-list';
|
||||
import { RemoteData } from '../../data/remote-data';
|
||||
|
||||
import { DSpaceObject } from '../../shared/dspace-object.model';
|
||||
import { DSPACE_OBJECT } from '../../shared/dspace-object.resource-type';
|
||||
import { HALLink } from '../../shared/hal-link.model';
|
||||
import { EPerson } from './eperson.model';
|
||||
import { EPERSON } from './eperson.resource-type';
|
||||
@@ -41,6 +42,7 @@ export class Group extends DSpaceObject {
|
||||
self: HALLink;
|
||||
subgroups: HALLink;
|
||||
epersons: HALLink;
|
||||
object: HALLink;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -57,4 +59,11 @@ export class Group extends DSpaceObject {
|
||||
@link(EPERSON, true)
|
||||
public epersons?: Observable<RemoteData<PaginatedList<EPerson>>>;
|
||||
|
||||
/**
|
||||
* Connected dspace object, the community or collection connected to a workflow group (204 no content for non-workflow groups)
|
||||
* Will be undefined unless the object {@link HALLink} has been resolved (can only be resolved for workflow groups)
|
||||
*/
|
||||
@link(DSPACE_OBJECT)
|
||||
public object?: Observable<RemoteData<DSpaceObject>>;
|
||||
|
||||
}
|
||||
|
@@ -24,6 +24,10 @@ export class ServerResponseService {
|
||||
return this.setStatus(401, message)
|
||||
}
|
||||
|
||||
setForbidden(message = 'Forbidden'): this {
|
||||
return this.setStatus(403, message)
|
||||
}
|
||||
|
||||
setNotFound(message = 'Not found'): this {
|
||||
return this.setStatus(404, message)
|
||||
}
|
||||
|
24
src/app/core/shared/collection.model.spec.ts
Normal file
24
src/app/core/shared/collection.model.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {Collection} from './collection.model';
|
||||
|
||||
describe('Collection', () => {
|
||||
|
||||
describe('Collection handle value', () => {
|
||||
|
||||
let metadataValue;
|
||||
|
||||
beforeEach(() => {
|
||||
metadataValue = {'dc.identifier.uri': [ { value: '123456789/1'}]};
|
||||
})
|
||||
|
||||
it('should return the handle value from metadata', () => {
|
||||
const community = Object.assign(new Collection(), { metadata: metadataValue });
|
||||
expect(community.handle).toEqual('123456789/1');
|
||||
});
|
||||
|
||||
it('should return undefined if the handle value from metadata is not present', () => {
|
||||
const community = Object.assign(new Collection(), { });
|
||||
expect(community.handle).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -1,4 +1,4 @@
|
||||
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
|
||||
import { deserialize, inheritSerialization } from 'cerialize';
|
||||
import { Observable } from 'rxjs';
|
||||
import { link, typedObject } from '../cache/builders/build-decorators';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
@@ -21,12 +21,6 @@ import { ChildHALResource } from './child-hal-resource.model';
|
||||
export class Collection extends DSpaceObject implements ChildHALResource {
|
||||
static type = COLLECTION;
|
||||
|
||||
/**
|
||||
* A string representing the unique handle of this Collection
|
||||
*/
|
||||
@autoserialize
|
||||
handle: string;
|
||||
|
||||
/**
|
||||
* The {@link HALLink}s for this Collection
|
||||
*/
|
||||
@@ -75,6 +69,13 @@ export class Collection extends DSpaceObject implements ChildHALResource {
|
||||
@link(COMMUNITY, false)
|
||||
parentCommunity?: Observable<RemoteData<Community>>;
|
||||
|
||||
/**
|
||||
* A string representing the unique handle of this Collection
|
||||
*/
|
||||
get handle(): string {
|
||||
return this.firstMetadataValue('dc.identifier.uri');
|
||||
}
|
||||
|
||||
/**
|
||||
* The introductory text of this Collection
|
||||
* Corresponds to the metadata field dc.description
|
||||
|
24
src/app/core/shared/community.model.spec.ts
Normal file
24
src/app/core/shared/community.model.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {Community} from './community.model';
|
||||
|
||||
describe('Community', () => {
|
||||
|
||||
describe('Community handle value', () => {
|
||||
|
||||
let metadataValue;
|
||||
|
||||
beforeEach(() => {
|
||||
metadataValue = {'dc.identifier.uri': [ { value: '123456789/1'}]};
|
||||
})
|
||||
|
||||
it('should return the handle value from metadata', () => {
|
||||
const community = Object.assign(new Community(), { metadata: metadataValue });
|
||||
expect(community.handle).toEqual('123456789/1');
|
||||
});
|
||||
|
||||
it('should return undefined if the handle value from metadata is not present', () => {
|
||||
const community = Object.assign(new Community(), { });
|
||||
expect(community.handle).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -1,4 +1,4 @@
|
||||
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
|
||||
import { deserialize, inheritSerialization } from 'cerialize';
|
||||
import { Observable } from 'rxjs';
|
||||
import { link, typedObject } from '../cache/builders/build-decorators';
|
||||
import { PaginatedList } from '../data/paginated-list';
|
||||
@@ -17,12 +17,6 @@ import { ChildHALResource } from './child-hal-resource.model';
|
||||
export class Community extends DSpaceObject implements ChildHALResource {
|
||||
static type = COMMUNITY;
|
||||
|
||||
/**
|
||||
* A string representing the unique handle of this Community
|
||||
*/
|
||||
@autoserialize
|
||||
handle: string;
|
||||
|
||||
/**
|
||||
* The {@link HALLink}s for this Community
|
||||
*/
|
||||
@@ -64,6 +58,13 @@ export class Community extends DSpaceObject implements ChildHALResource {
|
||||
@link(COMMUNITY, false)
|
||||
parentCommunity?: Observable<RemoteData<Community>>;
|
||||
|
||||
/**
|
||||
* A string representing the unique handle of this Community
|
||||
*/
|
||||
get handle(): string {
|
||||
return this.firstMetadataValue('dc.identifier.uri');
|
||||
}
|
||||
|
||||
/**
|
||||
* The introductory text of this Community
|
||||
* Corresponds to the metadata field dc.description
|
||||
|
@@ -14,7 +14,7 @@ import {
|
||||
getResourceLinksFromResponse,
|
||||
getResponseFromEntry,
|
||||
getSucceededRemoteData,
|
||||
redirectOn404Or401
|
||||
redirectOn4xx
|
||||
} from './operators';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { RemoteDataError } from '../data/remote-data-error';
|
||||
@@ -200,39 +200,67 @@ describe('Core Module - RxJS Operators', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('redirectOn404Or401', () => {
|
||||
describe('redirectOn4xx', () => {
|
||||
let router;
|
||||
let authService;
|
||||
|
||||
beforeEach(() => {
|
||||
router = jasmine.createSpyObj('router', ['navigateByUrl']);
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true),
|
||||
setRedirectUrl: {}
|
||||
});
|
||||
});
|
||||
|
||||
it('should call navigateByUrl to a 404 page, when the remote data contains a 404 error', () => {
|
||||
const testRD = createFailedRemoteDataObject(undefined, new RemoteDataError(404, 'Not Found', 'Object was not found'));
|
||||
|
||||
observableOf(testRD).pipe(redirectOn404Or401(router)).subscribe();
|
||||
observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/404', { skipLocationChange: true });
|
||||
});
|
||||
|
||||
it('should call navigateByUrl to a 401 page, when the remote data contains a 401 error', () => {
|
||||
const testRD = createFailedRemoteDataObject(undefined, new RemoteDataError(401, 'Unauthorized', 'The current user is unauthorized'));
|
||||
it('should call navigateByUrl to a 403 page, when the remote data contains a 403 error', () => {
|
||||
const testRD = createFailedRemoteDataObject(undefined, new RemoteDataError(403, 'Forbidden', 'Forbidden access'));
|
||||
|
||||
observableOf(testRD).pipe(redirectOn404Or401(router)).subscribe();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/401', { skipLocationChange: true });
|
||||
observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/403', { skipLocationChange: true });
|
||||
});
|
||||
|
||||
it('should not call navigateByUrl to a 404 or 401 page, when the remote data contains another error than a 404 or 401', () => {
|
||||
it('should not call navigateByUrl to a 404, 403 or 401 page, when the remote data contains another error than a 404, 403 or 401', () => {
|
||||
const testRD = createFailedRemoteDataObject(undefined, new RemoteDataError(500, 'Server Error', 'Something went wrong'));
|
||||
|
||||
observableOf(testRD).pipe(redirectOn404Or401(router)).subscribe();
|
||||
observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
|
||||
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call navigateByUrl to a 404 or 401 page, when the remote data contains no error', () => {
|
||||
it('should not call navigateByUrl to a 404, 403 or 401 page, when the remote data contains no error', () => {
|
||||
const testRD = createSuccessfulRemoteDataObject(undefined);
|
||||
|
||||
observableOf(testRD).pipe(redirectOn404Or401(router)).subscribe();
|
||||
observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
|
||||
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when the user is not authenticated', () => {
|
||||
beforeEach(() => {
|
||||
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
});
|
||||
|
||||
it('should set the redirect url and navigate to login when the remote data contains a 401 error', () => {
|
||||
const testRD = createFailedRemoteDataObject(undefined, new RemoteDataError(401, 'Unauthorized', 'The current user is unauthorized'));
|
||||
|
||||
observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
|
||||
expect(authService.setRedirectUrl).toHaveBeenCalled();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('login');
|
||||
});
|
||||
|
||||
it('should set the redirect url and navigate to login when the remote data contains a 403 error', () => {
|
||||
const testRD = createFailedRemoteDataObject(undefined, new RemoteDataError(403, 'Forbidden', 'Forbidden access'));
|
||||
|
||||
observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
|
||||
expect(authService.setRedirectUrl).toHaveBeenCalled();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('login');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getResponseFromEntry', () => {
|
||||
|
@@ -13,8 +13,9 @@ import { MetadataField } from '../metadata/metadata-field.model';
|
||||
import { MetadataSchema } from '../metadata/metadata-schema.model';
|
||||
import { BrowseDefinition } from './browse-definition.model';
|
||||
import { DSpaceObject } from './dspace-object.model';
|
||||
import { getPageNotFoundRoute, getUnauthorizedRoute } from '../../app-routing-paths';
|
||||
import { getForbiddenRoute, getPageNotFoundRoute } from '../../app-routing-paths';
|
||||
import { getEndUserAgreementPath } from '../../info/info-routing-paths';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
|
||||
/**
|
||||
* This file contains custom RxJS operators that can be used in multiple places
|
||||
@@ -178,29 +179,47 @@ export const getAllSucceededRemoteListPayload = () =>
|
||||
* Operator that checks if a remote data object returned a 401 or 404 error
|
||||
* When it does contain such an error, it will redirect the user to the related error page, without altering the current URL
|
||||
* @param router The router used to navigate to a new page
|
||||
* @param authService Service to check if the user is authenticated
|
||||
*/
|
||||
export const redirectOn404Or401 = (router: Router) =>
|
||||
export const redirectOn4xx = (router: Router, authService: AuthService) =>
|
||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||
source.pipe(
|
||||
tap((rd: RemoteData<T>) => {
|
||||
observableCombineLatest(source, authService.isAuthenticated()).pipe(
|
||||
map(([rd, isAuthenticated]: [RemoteData<T>, boolean]) => {
|
||||
if (rd.hasFailed) {
|
||||
if (rd.error.statusCode === 404) {
|
||||
router.navigateByUrl(getPageNotFoundRoute(), {skipLocationChange: true});
|
||||
} else if (rd.error.statusCode === 401) {
|
||||
router.navigateByUrl(getUnauthorizedRoute(), {skipLocationChange: true});
|
||||
} else if (rd.error.statusCode === 403 || rd.error.statusCode === 401) {
|
||||
if (isAuthenticated) {
|
||||
router.navigateByUrl(getForbiddenRoute(), {skipLocationChange: true});
|
||||
} else {
|
||||
authService.setRedirectUrl(router.url);
|
||||
router.navigateByUrl('login');
|
||||
}
|
||||
}
|
||||
}
|
||||
return rd;
|
||||
}));
|
||||
|
||||
/**
|
||||
* Operator that returns a UrlTree to the unauthorized page when the boolean received is false
|
||||
* @param router
|
||||
* Operator that returns a UrlTree to a forbidden page or the login page when the boolean received is false
|
||||
* @param router The router used to navigate to a forbidden page
|
||||
* @param authService The AuthService used to determine whether or not the user is logged in
|
||||
* @param redirectUrl The URL to redirect back to after logging in
|
||||
*/
|
||||
export const returnUnauthorizedUrlTreeOnFalse = (router: Router) =>
|
||||
export const returnForbiddenUrlTreeOrLoginOnFalse = (router: Router, authService: AuthService, redirectUrl: string) =>
|
||||
(source: Observable<boolean>): Observable<boolean | UrlTree> =>
|
||||
source.pipe(
|
||||
map((authorized: boolean) => {
|
||||
return authorized ? authorized : router.parseUrl(getUnauthorizedRoute())
|
||||
observableCombineLatest(source, authService.isAuthenticated()).pipe(
|
||||
map(([authorized, authenticated]: [boolean, boolean]) => {
|
||||
if (authorized) {
|
||||
return authorized;
|
||||
} else {
|
||||
if (authenticated) {
|
||||
return router.parseUrl(getForbiddenRoute());
|
||||
} else {
|
||||
authService.setRedirectUrl(redirectUrl);
|
||||
return router.parseUrl('login');
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
|
9
src/app/core/shared/process-output.resource-type.ts
Normal file
9
src/app/core/shared/process-output.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ResourceType } from './resource-type';
|
||||
|
||||
/**
|
||||
* The resource type for ProcessOutput
|
||||
*
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
export const PROCESS_OUTPUT_TYPE = new ResourceType('processOutput');
|
10
src/app/forbidden/forbidden.component.html
Normal file
10
src/app/forbidden/forbidden.component.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="forbidden container">
|
||||
<h1>403</h1>
|
||||
<h2><small>{{"403.forbidden" | translate}}</small></h2>
|
||||
<br/>
|
||||
<p>{{"403.help" | translate}}</p>
|
||||
<br/>
|
||||
<p class="text-center">
|
||||
<a routerLink="/home" class="btn btn-primary">{{"403.link.home-page" | translate}}</a>
|
||||
</p>
|
||||
</div>
|
@@ -3,30 +3,30 @@ import { AuthService } from '../core/auth/auth.service';
|
||||
import { ServerResponseService } from '../core/services/server-response.service';
|
||||
|
||||
/**
|
||||
* This component representing the `Unauthorized` DSpace page.
|
||||
* This component representing the `Forbidden` DSpace page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-unauthorized',
|
||||
templateUrl: './unauthorized.component.html',
|
||||
styleUrls: ['./unauthorized.component.scss']
|
||||
selector: 'ds-forbidden',
|
||||
templateUrl: './forbidden.component.html',
|
||||
styleUrls: ['./forbidden.component.scss']
|
||||
})
|
||||
export class UnauthorizedComponent implements OnInit {
|
||||
export class ForbiddenComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
* @param {AuthService} authservice
|
||||
* @param {AuthService} authService
|
||||
* @param {ServerResponseService} responseService
|
||||
*/
|
||||
constructor(private authservice: AuthService, private responseService: ServerResponseService) {
|
||||
this.responseService.setUnauthorized();
|
||||
constructor(private authService: AuthService, private responseService: ServerResponseService) {
|
||||
this.responseService.setForbidden();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove redirect url from the state
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.authservice.clearRedirectUrl();
|
||||
this.authService.clearRedirectUrl();
|
||||
}
|
||||
|
||||
}
|
@@ -17,7 +17,7 @@
|
||||
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files" [title]="'process.detail.output-files'">
|
||||
<ds-file-download-link *ngFor="let file of files; let last=last;" [href]="file?._links?.content?.href" [download]="getFileName(file)">
|
||||
<span>{{getFileName(file)}}</span>
|
||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||
</ds-file-download-link>
|
||||
</ds-process-detail-field>
|
||||
</div>
|
||||
@@ -34,9 +34,20 @@
|
||||
<div>{{ process.processStatus }}</div>
|
||||
</ds-process-detail-field>
|
||||
|
||||
<!--<ds-process-detail-field id="process-output" [title]="'process.detail.output'">-->
|
||||
<!--<pre class="font-weight-bold text-secondary bg-light p-3">{{'process.detail.output.alert' | translate}}</pre>-->
|
||||
<!--</ds-process-detail-field>-->
|
||||
<ds-process-detail-field *ngIf="isProcessFinished(process)" id="process-output" [title]="'process.detail.output'">
|
||||
<button *ngIf="!showOutputLogs && process?._links?.output?.href != undefined" id="showOutputButton" class="btn btn-light" (click)="showProcessOutputLogs()">
|
||||
{{ 'process.detail.logs.button' | translate }}
|
||||
</button>
|
||||
<ds-loading *ngIf="retrievingOutputLogs$ | async" class="ds-loading" message="{{ 'process.detail.logs.loading' | translate }}"></ds-loading>
|
||||
<pre class="font-weight-bold text-secondary bg-light p-3"
|
||||
*ngIf="showOutputLogs && (outputLogs$ | async)?.length > 0">{{ (outputLogs$ | async) }}</pre>
|
||||
<p id="no-output-logs-message" *ngIf="(!(retrievingOutputLogs$ | async) && showOutputLogs)
|
||||
&& !(outputLogs$ | async) || (outputLogs$ | async)?.length == 0 || !process._links.output">
|
||||
{{ 'process.detail.logs.none' | translate }}
|
||||
</p>
|
||||
</ds-process-detail-field>
|
||||
|
||||
<a class="btn btn-light mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
|
||||
<div>
|
||||
<a class="btn btn-light mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,9 +1,22 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||
import { AuthServiceMock } from '../../shared/mocks/auth.service.mock';
|
||||
import { ProcessDetailComponent } from './process-detail.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import {
|
||||
async,
|
||||
ComponentFixture,
|
||||
discardPeriodicTasks,
|
||||
fakeAsync,
|
||||
flush,
|
||||
flushMicrotasks,
|
||||
TestBed,
|
||||
tick
|
||||
} from '@angular/core/testing';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ProcessDetailFieldComponent } from './process-detail-field/process-detail-field.component';
|
||||
import { Process } from '../processes/process.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
@@ -22,15 +35,21 @@ describe('ProcessDetailComponent', () => {
|
||||
|
||||
let processService: ProcessDataService;
|
||||
let nameService: DSONameService;
|
||||
let bitstreamDataService: BitstreamDataService;
|
||||
let httpClient: HttpClient;
|
||||
|
||||
let process: Process;
|
||||
let fileName: string;
|
||||
let files: Bitstream[];
|
||||
|
||||
let processOutput;
|
||||
|
||||
function init() {
|
||||
processOutput = 'Process Started'
|
||||
process = Object.assign(new Process(), {
|
||||
processId: 1,
|
||||
scriptName: 'script-name',
|
||||
processStatus: 'COMPLETED',
|
||||
parameters: [
|
||||
{
|
||||
name: '-f',
|
||||
@@ -40,7 +59,15 @@ describe('ProcessDetailComponent', () => {
|
||||
name: '-i',
|
||||
value: 'identifier'
|
||||
}
|
||||
]
|
||||
],
|
||||
_links: {
|
||||
self: {
|
||||
href: 'https://rest.api/processes/1'
|
||||
},
|
||||
output: {
|
||||
href: 'https://rest.api/processes/1/output'
|
||||
}
|
||||
}
|
||||
});
|
||||
fileName = 'fake-file-name';
|
||||
files = [
|
||||
@@ -59,12 +86,24 @@ describe('ProcessDetailComponent', () => {
|
||||
}
|
||||
})
|
||||
];
|
||||
const logBitstream = Object.assign(new Bitstream(), {
|
||||
id: 'output.log',
|
||||
_links: {
|
||||
content: { href: 'log-selflink' }
|
||||
}
|
||||
});
|
||||
processService = jasmine.createSpyObj('processService', {
|
||||
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files))
|
||||
});
|
||||
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
||||
findByHref: createSuccessfulRemoteDataObject$(logBitstream)
|
||||
});
|
||||
nameService = jasmine.createSpyObj('nameService', {
|
||||
getName: fileName
|
||||
});
|
||||
httpClient = jasmine.createSpyObj('httpClient', {
|
||||
get: observableOf(processOutput)
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async(() => {
|
||||
@@ -73,26 +112,41 @@ describe('ProcessDetailComponent', () => {
|
||||
declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }) } },
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }) }
|
||||
},
|
||||
{ provide: ProcessDataService, useValue: processService },
|
||||
{ provide: DSONameService, useValue: nameService }
|
||||
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
||||
{ provide: DSONameService, useValue: nameService },
|
||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||
{ provide: HttpClient, useValue: httpClient },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProcessDetailComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
afterEach(fakeAsync(() => {
|
||||
TestBed.resetTestingModule();
|
||||
fixture.destroy();
|
||||
flush();
|
||||
flushMicrotasks();
|
||||
discardPeriodicTasks();
|
||||
component = null;
|
||||
}));
|
||||
|
||||
it('should display the script\'s name', () => {
|
||||
fixture.detectChanges();
|
||||
const name = fixture.debugElement.query(By.css('#process-name')).nativeElement;
|
||||
expect(name.textContent).toContain(process.scriptName);
|
||||
});
|
||||
|
||||
it('should display the process\'s parameters', () => {
|
||||
fixture.detectChanges();
|
||||
const args = fixture.debugElement.query(By.css('#process-arguments')).nativeElement;
|
||||
process.parameters.forEach((param) => {
|
||||
expect(args.textContent).toContain(`${param.name} ${param.value}`)
|
||||
@@ -100,8 +154,57 @@ describe('ProcessDetailComponent', () => {
|
||||
});
|
||||
|
||||
it('should display the process\'s output files', () => {
|
||||
fixture.detectChanges();
|
||||
const processFiles = fixture.debugElement.query(By.css('#process-files')).nativeElement;
|
||||
expect(processFiles.textContent).toContain(fileName);
|
||||
});
|
||||
|
||||
describe('if press show output logs', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
spyOn(component, 'showProcessOutputLogs').and.callThrough();
|
||||
fixture.detectChanges();
|
||||
|
||||
const showOutputButton = fixture.debugElement.query(By.css('#showOutputButton'));
|
||||
showOutputButton.triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
tick();
|
||||
}));
|
||||
it('should trigger showProcessOutputLogs', () => {
|
||||
expect(component.showProcessOutputLogs).toHaveBeenCalled();
|
||||
});
|
||||
it('should display the process\'s output logs', () => {
|
||||
fixture.detectChanges();
|
||||
const outputProcess = fixture.debugElement.query(By.css('#process-output pre'));
|
||||
expect(outputProcess.nativeElement.textContent).toContain(processOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if press show output logs and process has no output logs', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
jasmine.getEnv().allowRespy(true);
|
||||
spyOn(httpClient, 'get').and.returnValue(observableOf(null));
|
||||
fixture = TestBed.createComponent(ProcessDetailComponent);
|
||||
component = fixture.componentInstance;
|
||||
spyOn(component, 'showProcessOutputLogs').and.callThrough();
|
||||
fixture.detectChanges();
|
||||
const showOutputButton = fixture.debugElement.query(By.css('#showOutputButton'));
|
||||
showOutputButton.triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('should not display the process\'s output logs', () => {
|
||||
const outputProcess = fixture.debugElement.query(By.css('#process-output pre'));
|
||||
expect(outputProcess).toBeNull();
|
||||
});
|
||||
it('should display message saying there are no output logs', () => {
|
||||
const noOutputProcess = fixture.debugElement.query(By.css('#no-output-logs-message')).nativeElement;
|
||||
expect(noOutputProcess).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -1,15 +1,23 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, NgZone, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Process } from '../processes/process.model';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { getFirstSucceededRemoteDataPayload, redirectOn404Or401 } from '../../core/shared/operators';
|
||||
import { AlertType } from '../../shared/alert/aletr-type';
|
||||
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||
import { PaginatedList } from '../../core/data/paginated-list';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { finalize, map, mergeMap, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||
import { PaginatedList } from '../../core/data/paginated-list';
|
||||
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { getFirstSucceededRemoteDataPayload, redirectOn4xx } from '../../core/shared/operators';
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import { AlertType } from '../../shared/alert/aletr-type';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { ProcessStatus } from '../processes/process-status.model';
|
||||
import { Process } from '../processes/process.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-process-detail',
|
||||
@@ -36,10 +44,33 @@ export class ProcessDetailComponent implements OnInit {
|
||||
*/
|
||||
filesRD$: Observable<RemoteData<PaginatedList<Bitstream>>>;
|
||||
|
||||
/**
|
||||
* File link that contain the output logs with auth token
|
||||
*/
|
||||
outputLogFileUrl$: Observable<string>;
|
||||
|
||||
/**
|
||||
* The Process's Output logs
|
||||
*/
|
||||
outputLogs$: Observable<string>;
|
||||
|
||||
/**
|
||||
* Boolean on whether or not to show the output logs
|
||||
*/
|
||||
showOutputLogs;
|
||||
/**
|
||||
* When it's retrieving the output logs from backend, to show loading component
|
||||
*/
|
||||
retrievingOutputLogs$: BehaviorSubject<boolean>;
|
||||
|
||||
constructor(protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
protected processService: ProcessDataService,
|
||||
protected nameService: DSONameService) {
|
||||
protected bitstreamDataService: BitstreamDataService,
|
||||
protected nameService: DSONameService,
|
||||
private zone: NgZone,
|
||||
protected authService: AuthService,
|
||||
protected http: HttpClient) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,9 +78,13 @@ export class ProcessDetailComponent implements OnInit {
|
||||
* Display a 404 if the process doesn't exist
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.showOutputLogs = false;
|
||||
this.retrievingOutputLogs$ = new BehaviorSubject<boolean>(false);
|
||||
this.processRD$ = this.route.data.pipe(
|
||||
map((data) => data.process as RemoteData<Process>),
|
||||
redirectOn404Or401(this.router)
|
||||
map((data) => {
|
||||
return data.process as RemoteData<Process>
|
||||
}),
|
||||
redirectOn4xx(this.router, this.authService)
|
||||
);
|
||||
|
||||
this.filesRD$ = this.processRD$.pipe(
|
||||
@@ -63,7 +98,68 @@ export class ProcessDetailComponent implements OnInit {
|
||||
* @param bitstream
|
||||
*/
|
||||
getFileName(bitstream: Bitstream) {
|
||||
return this.nameService.getName(bitstream);
|
||||
return bitstream instanceof DSpaceObject ? this.nameService.getName(bitstream) : 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the process logs, while setting the loading subject to true.
|
||||
* Sets the outputLogs when retrieved and sets the showOutputLogs boolean to show them and hide the button.
|
||||
*/
|
||||
showProcessOutputLogs() {
|
||||
this.retrievingOutputLogs$.next(true);
|
||||
this.zone.runOutsideAngular(() => {
|
||||
const processOutputRD$: Observable<RemoteData<Bitstream>> = this.processRD$.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((process: Process) => {
|
||||
return this.bitstreamDataService.findByHref(process._links.output.href);
|
||||
})
|
||||
);
|
||||
this.outputLogFileUrl$ = processOutputRD$.pipe(
|
||||
tap((processOutputFileRD: RemoteData<Bitstream>) => {
|
||||
if (processOutputFileRD.statusCode === 204) {
|
||||
this.zone.run(() => this.retrievingOutputLogs$.next(false));
|
||||
this.showOutputLogs = true;
|
||||
}
|
||||
}),
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
mergeMap((processOutput: Bitstream) => {
|
||||
const url = processOutput._links.content.href;
|
||||
return this.authService.getShortlivedToken().pipe(take(1),
|
||||
map((token: string) => {
|
||||
return hasValue(token) ? new URLCombiner(url, `?authentication-token=${token}`).toString() : url;
|
||||
}));
|
||||
})
|
||||
)
|
||||
});
|
||||
this.outputLogs$ = this.outputLogFileUrl$.pipe(take(1),
|
||||
mergeMap((url: string) => {
|
||||
return this.getTextFile(url);
|
||||
}),
|
||||
finalize(() => this.zone.run(() => this.retrievingOutputLogs$.next(false))),
|
||||
);
|
||||
this.outputLogs$.pipe(take(1)).subscribe();
|
||||
}
|
||||
|
||||
getTextFile(filename: string): Observable<string> {
|
||||
// The Observable returned by get() is of type Observable<string>
|
||||
// because a text response was specified.
|
||||
// There's no need to pass a <string> type parameter to get().
|
||||
return this.http.get(filename, { responseType: 'text' })
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.showOutputLogs = true;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the given process has Completed or Failed status
|
||||
* @param process Process to check if completed or failed
|
||||
*/
|
||||
isProcessFinished(process: Process): boolean {
|
||||
return (hasValue(process) && hasValue(process.processStatus) &&
|
||||
(process.processStatus.toString() === ProcessStatus[ProcessStatus.COMPLETED].toString()
|
||||
|| process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()));
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { PROCESS_OUTPUT_TYPE } from '../../core/shared/process-output.resource-type';
|
||||
import { ProcessStatus } from './process-status.model';
|
||||
import { ProcessParameter } from './process-parameter.model';
|
||||
import { CacheableObject } from '../../core/cache/object-cache.reducer';
|
||||
@@ -85,4 +87,11 @@ export class Process implements CacheableObject {
|
||||
*/
|
||||
@link(SCRIPT)
|
||||
script?: Observable<RemoteData<Script>>;
|
||||
|
||||
/**
|
||||
* The output logs created by this Process
|
||||
* Will be undefined unless the output {@link HALLink} has been resolved.
|
||||
*/
|
||||
@link(PROCESS_OUTPUT_TYPE)
|
||||
output?: Observable<RemoteData<Bitstream>>;
|
||||
}
|
||||
|
@@ -177,7 +177,7 @@ describe('ProfilePageComponent', () => {
|
||||
component.setPasswordValue('testest');
|
||||
component.setInvalid(false);
|
||||
|
||||
operations = [{op: 'replace', path: '/password', value: 'testest'}];
|
||||
operations = [{op: 'add', path: '/password', value: 'testest'}];
|
||||
result = component.updateSecurity();
|
||||
});
|
||||
|
||||
|
@@ -120,7 +120,7 @@ export class ProfilePageComponent implements OnInit {
|
||||
this.notificationsService.error(this.translate.instant(this.PASSWORD_NOTIFICATIONS_PREFIX + 'error.general'));
|
||||
}
|
||||
if (!this.invalidSecurity && passEntered) {
|
||||
const operation = Object.assign({op: 'replace', path: '/password', value: this.password});
|
||||
const operation = Object.assign({op: 'add', path: '/password', value: this.password});
|
||||
this.epersonService.patch(this.currentUser, [operation]).subscribe((response: RestResponse) => {
|
||||
if (response.isSuccessful) {
|
||||
this.notificationsService.success(
|
||||
|
@@ -0,0 +1,48 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ComcolPageHandleComponent } from './comcol-page-handle.component';
|
||||
|
||||
const handle = 'http://localhost:4000/handle/123456789/2';
|
||||
|
||||
describe('ComcolPageHandleComponent', () => {
|
||||
let component: ComcolPageHandleComponent;
|
||||
let fixture: ComponentFixture<ComcolPageHandleComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ ComcolPageHandleComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ComcolPageHandleComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should be empty if no content is passed', () => {
|
||||
component.content = undefined;
|
||||
fixture.detectChanges();
|
||||
const div = fixture.debugElement.query(By.css('div'));
|
||||
expect(div).toBeNull();
|
||||
});
|
||||
|
||||
it('should create a link pointing the handle when present', () => {
|
||||
|
||||
component.content = handle;
|
||||
fixture.detectChanges();
|
||||
|
||||
const link = fixture.debugElement.query(By.css('a'));
|
||||
expect(link.nativeElement.getAttribute('href')).toBe(handle);
|
||||
expect(link.nativeElement.innerHTML).toBe(handle);
|
||||
|
||||
});
|
||||
|
||||
});
|
@@ -1,5 +1,4 @@
|
||||
import { Component, Injectable, Input } from '@angular/core';
|
||||
import { UIURLCombiner } from '../../core/url-combiner/ui-url-combiner';
|
||||
|
||||
/**
|
||||
* This component builds a URL from the value of "handle"
|
||||
@@ -21,6 +20,6 @@ export class ComcolPageHandleComponent {
|
||||
@Input() content: string;
|
||||
|
||||
public getHandle(): string {
|
||||
return new UIURLCombiner('/handle/', this.content).toString();
|
||||
return this.content;
|
||||
}
|
||||
}
|
||||
|
@@ -53,12 +53,26 @@ describe('ExportMetadataSelectorComponent', () => {
|
||||
const mockCollection: Collection = Object.assign(new Collection(), {
|
||||
id: 'test-collection-1-1',
|
||||
name: 'test-collection-1',
|
||||
handle: 'fake/test-collection-1',
|
||||
metadata: {
|
||||
'dc.identifier.uri': [
|
||||
{
|
||||
language: null,
|
||||
value: 'fake/test-collection-1'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const mockCommunity = Object.assign(new Community(), {
|
||||
id: 'test-uuid',
|
||||
handle: 'fake/test-community-1',
|
||||
metadata: {
|
||||
'dc.identifier.uri': [
|
||||
{
|
||||
language: null,
|
||||
value: 'fake/test-community-1'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const itemRD = createSuccessfulRemoteDataObject(mockItem);
|
||||
|
@@ -1,15 +1,14 @@
|
||||
<form [formGroup]="form" (ngSubmit)="onSubmit(value)"
|
||||
<form #form="ngForm" (ngSubmit)="onSubmit(value)"
|
||||
[action]="action" (keydown)="onKeydown($event)"
|
||||
(keydown.arrowdown)="shiftFocusDown($event)"
|
||||
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
|
||||
(dsClickOutside)="checkIfValidInput(form);close();">
|
||||
<input #inputField type="text" formControlName="metadataNameField" [(ngModel)]="value" id="name" [name]="name"
|
||||
(dsClickOutside)="close();">
|
||||
<input #inputField type="text" [(ngModel)]="value" [name]="name"
|
||||
class="form-control suggestion_input"
|
||||
[ngClass]="{'is-invalid': !valid}"
|
||||
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
|
||||
[placeholder]="placeholder"
|
||||
ng-model-options="{standalone: true}"
|
||||
autocomplete="off">
|
||||
[ngModelOptions]="{standalone: true}" autocomplete="off"/>
|
||||
<input type="submit" class="d-none"/>
|
||||
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
||||
<div class="dropdown-list">
|
||||
@@ -20,5 +19,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</form>
|
@@ -3,11 +3,9 @@ import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angula
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { MetadataFieldDataService } from '../../../core/data/metadata-field-data.service';
|
||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||
import { FilterInputSuggestionsComponent } from './filter-input-suggestions.component';
|
||||
|
||||
describe('FilterInputSuggestionsComponent', () => {
|
||||
@@ -23,13 +21,9 @@ describe('FilterInputSuggestionsComponent', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule, ReactiveFormsModule],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule],
|
||||
declarations: [FilterInputSuggestionsComponent],
|
||||
providers: [FormsModule,
|
||||
ReactiveFormsModule,
|
||||
{ provide: MetadataFieldDataService, useValue: {} },
|
||||
{ provide: ObjectUpdatesService, useValue: {} },
|
||||
],
|
||||
providers: [],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(FilterInputSuggestionsComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
|
@@ -1,8 +1,5 @@
|
||||
import { Component, forwardRef, Input, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
|
||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||
import { MetadatumViewModel } from '../../../core/shared/metadata.models';
|
||||
import { MetadataFieldValidator } from '../../utils/metadatafield-validator.directive';
|
||||
import { Component, forwardRef, Input } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { InputSuggestionsComponent } from '../input-suggestions.component';
|
||||
import { InputSuggestion } from '../input-suggestions.model';
|
||||
|
||||
@@ -24,39 +21,12 @@ import { InputSuggestion } from '../input-suggestions.model';
|
||||
/**
|
||||
* Component representing a form with a autocomplete functionality
|
||||
*/
|
||||
export class FilterInputSuggestionsComponent extends InputSuggestionsComponent implements OnInit {
|
||||
|
||||
form: FormGroup;
|
||||
|
||||
/**
|
||||
* The current url of this page
|
||||
*/
|
||||
@Input() url: string;
|
||||
|
||||
/**
|
||||
* The metadatum of this field
|
||||
*/
|
||||
@Input() metadata: MetadatumViewModel;
|
||||
|
||||
export class FilterInputSuggestionsComponent extends InputSuggestionsComponent {
|
||||
/**
|
||||
* The suggestions that should be shown
|
||||
*/
|
||||
@Input() suggestions: InputSuggestion[] = [];
|
||||
|
||||
constructor(private metadataFieldValidator: MetadataFieldValidator,
|
||||
private objectUpdatesService: ObjectUpdatesService) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.form = new FormGroup({
|
||||
metadataNameField: new FormControl(this._value, {
|
||||
asyncValidators: [this.metadataFieldValidator.validate.bind(this.metadataFieldValidator)],
|
||||
validators: [Validators.required]
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(data) {
|
||||
this.value = data;
|
||||
this.submitSuggestion.emit(data);
|
||||
@@ -70,15 +40,4 @@ export class FilterInputSuggestionsComponent extends InputSuggestionsComponent i
|
||||
this.queryInput.nativeElement.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the input is valid according to validator and send (in)valid state to store
|
||||
* @param form Form with input
|
||||
*/
|
||||
checkIfValidInput(form) {
|
||||
this.valid = !(form.get('metadataNameField').status === 'INVALID' && (form.get('metadataNameField').dirty || form.get('metadataNameField').touched));
|
||||
this.objectUpdatesService.setValidFieldUpdate(this.url, this.metadata.uuid, this.valid);
|
||||
return this.valid;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,24 @@
|
||||
<form [formGroup]="form" (ngSubmit)="onSubmit(value)"
|
||||
[action]="action" (keydown)="onKeydown($event)"
|
||||
(keydown.arrowdown)="shiftFocusDown($event)"
|
||||
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
|
||||
(dsClickOutside)="checkIfValidInput(form);close();">
|
||||
<input #inputField type="text" formControlName="metadataNameField" [(ngModel)]="value" id="name" [name]="name"
|
||||
class="form-control suggestion_input"
|
||||
[ngClass]="{'is-invalid': !valid}"
|
||||
[dsDebounce]="debounceTime" (onDebounce)="find($event)"
|
||||
[placeholder]="placeholder"
|
||||
ng-model-options="{standalone: true}"
|
||||
autocomplete="off">
|
||||
<input type="submit" class="d-none"/>
|
||||
<div class="autocomplete dropdown-menu" [ngClass]="{'show': (show | async) && isNotEmpty(suggestions)}">
|
||||
<div class="dropdown-list">
|
||||
<div *ngFor="let suggestionOption of suggestions">
|
||||
<a href="#" class="d-block dropdown-item" (click)="onClickSuggestion(suggestionOption.value)" #suggestion>
|
||||
<span [innerHTML]="suggestionOption.displayValue"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
@@ -0,0 +1,63 @@
|
||||
import { ChangeDetectionStrategy, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { MetadataFieldDataService } from '../../../core/data/metadata-field-data.service';
|
||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||
import { ValidationSuggestionsComponent } from './validation-suggestions.component';
|
||||
|
||||
describe('ValidationSuggestionsComponent', () => {
|
||||
|
||||
let comp: ValidationSuggestionsComponent;
|
||||
let fixture: ComponentFixture<ValidationSuggestionsComponent>;
|
||||
let de: DebugElement;
|
||||
let el: HTMLElement;
|
||||
const suggestions = [{ displayValue: 'suggestion uno', value: 'suggestion uno' }, {
|
||||
displayValue: 'suggestion dos',
|
||||
value: 'suggestion dos'
|
||||
}, { displayValue: 'suggestion tres', value: 'suggestion tres' }];
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule, ReactiveFormsModule],
|
||||
declarations: [ValidationSuggestionsComponent],
|
||||
providers: [FormsModule,
|
||||
ReactiveFormsModule,
|
||||
{ provide: MetadataFieldDataService, useValue: {} },
|
||||
{ provide: ObjectUpdatesService, useValue: {} },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(ValidationSuggestionsComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ValidationSuggestionsComponent);
|
||||
|
||||
comp = fixture.componentInstance; // LoadingComponent test instance
|
||||
comp.suggestions = suggestions;
|
||||
// query for the message <label> by CSS element selector
|
||||
de = fixture.debugElement;
|
||||
el = de.nativeElement;
|
||||
comp.show.next(true);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('when an element is clicked', () => {
|
||||
const clickedIndex = 0;
|
||||
beforeEach(() => {
|
||||
spyOn(comp, 'onClickSuggestion');
|
||||
const clickedLink = de.query(By.css('.dropdown-list > div:nth-child(' + (clickedIndex + 1) + ') a'));
|
||||
clickedLink.triggerEventHandler('click', {});
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('should call onClickSuggestion() with the suggestion as a parameter', () => {
|
||||
expect(comp.onClickSuggestion).toHaveBeenCalledWith(suggestions[clickedIndex].value);
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,84 @@
|
||||
import { Component, forwardRef, Input, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
|
||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||
import { MetadatumViewModel } from '../../../core/shared/metadata.models';
|
||||
import { MetadataFieldValidator } from '../../utils/metadatafield-validator.directive';
|
||||
import { InputSuggestionsComponent } from '../input-suggestions.component';
|
||||
import { InputSuggestion } from '../input-suggestions.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-validation-suggestions',
|
||||
styleUrls: ['./../input-suggestions.component.scss'],
|
||||
templateUrl: './validation-suggestions.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
// Usage of forwardRef necessary https://github.com/angular/angular.io/issues/1151
|
||||
// tslint:disable-next-line:no-forward-ref
|
||||
useExisting: forwardRef(() => ValidationSuggestionsComponent),
|
||||
multi: true
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* Component representing a form with a autocomplete functionality
|
||||
*/
|
||||
export class ValidationSuggestionsComponent extends InputSuggestionsComponent implements OnInit {
|
||||
|
||||
form: FormGroup;
|
||||
|
||||
/**
|
||||
* The current url of this page
|
||||
*/
|
||||
@Input() url: string;
|
||||
|
||||
/**
|
||||
* The metadatum of this field
|
||||
*/
|
||||
@Input() metadata: MetadatumViewModel;
|
||||
|
||||
/**
|
||||
* The suggestions that should be shown
|
||||
*/
|
||||
@Input() suggestions: InputSuggestion[] = [];
|
||||
|
||||
constructor(private metadataFieldValidator: MetadataFieldValidator,
|
||||
private objectUpdatesService: ObjectUpdatesService) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.form = new FormGroup({
|
||||
metadataNameField: new FormControl(this._value, {
|
||||
asyncValidators: [this.metadataFieldValidator.validate.bind(this.metadataFieldValidator)],
|
||||
validators: [Validators.required]
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(data) {
|
||||
this.value = data;
|
||||
this.submitSuggestion.emit(data);
|
||||
}
|
||||
|
||||
onClickSuggestion(data) {
|
||||
this.value = data;
|
||||
this.clickSuggestion.emit(data);
|
||||
this.close();
|
||||
this.blockReopen = true;
|
||||
this.queryInput.nativeElement.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the input is valid according to validator and send (in)valid state to store
|
||||
* @param form Form with input
|
||||
*/
|
||||
checkIfValidInput(form) {
|
||||
this.valid = !(form.get('metadataNameField').status === 'INVALID' && (form.get('metadataNameField').dirty || form.get('metadataNameField').touched));
|
||||
this.objectUpdatesService.setValidFieldUpdate(this.url, this.metadata.uuid, this.valid);
|
||||
return this.valid;
|
||||
}
|
||||
|
||||
}
|
@@ -1,4 +1,6 @@
|
||||
/* tslint:disable:no-empty */
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
export class AuthServiceMock {
|
||||
public checksAuthenticationToken() {
|
||||
return
|
||||
@@ -6,4 +8,15 @@ export class AuthServiceMock {
|
||||
public buildAuthHeader() {
|
||||
return 'auth-header';
|
||||
}
|
||||
|
||||
public getShortlivedToken(): Observable<string> {
|
||||
return observableOf('token');
|
||||
}
|
||||
|
||||
public isAuthenticated(): Observable<boolean> {
|
||||
return observableOf(true);
|
||||
}
|
||||
|
||||
public setRedirectUrl(url: string) {
|
||||
}
|
||||
}
|
||||
|
@@ -109,7 +109,6 @@ describe('SearchFormComponent', () => {
|
||||
|
||||
export const objects: DSpaceObject[] = [
|
||||
Object.assign(new Community(), {
|
||||
handle: '10673/11',
|
||||
logo: {
|
||||
self: {
|
||||
_isScalar: true,
|
||||
@@ -162,12 +161,17 @@ export const objects: DSpaceObject[] = [
|
||||
language: null,
|
||||
value: 'OR2017 - Demonstration'
|
||||
}
|
||||
]
|
||||
],
|
||||
'dc.identifier.uri': [
|
||||
{
|
||||
language: null,
|
||||
value: 'http://localhost:4000/handle/10673/11'
|
||||
}
|
||||
],
|
||||
}
|
||||
}),
|
||||
Object.assign(new Community(),
|
||||
{
|
||||
handle: '10673/1',
|
||||
logo: {
|
||||
self: {
|
||||
_isScalar: true,
|
||||
@@ -220,7 +224,13 @@ export const objects: DSpaceObject[] = [
|
||||
language: null,
|
||||
value: 'Sample Community'
|
||||
}
|
||||
]
|
||||
],
|
||||
'dc.identifier.uri': [
|
||||
{
|
||||
language: null,
|
||||
value: 'http://localhost:4000/handle/10673/1'
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@@ -96,7 +96,6 @@ describe('SearchResultsComponent', () => {
|
||||
|
||||
export const objects = [
|
||||
Object.assign(new Community(), {
|
||||
handle: '10673/11',
|
||||
logo: {
|
||||
self: {
|
||||
_isScalar: true,
|
||||
@@ -149,12 +148,17 @@ export const objects = [
|
||||
language: null,
|
||||
value: 'OR2017 - Demonstration'
|
||||
}
|
||||
],
|
||||
'dc.identifier.uri': [
|
||||
{
|
||||
language: null,
|
||||
value: 'http://localhost:4000/handle/10673/11'
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
Object.assign(new Community(),
|
||||
{
|
||||
handle: '10673/1',
|
||||
logo: {
|
||||
self: {
|
||||
_isScalar: true,
|
||||
@@ -207,6 +211,12 @@ export const objects = [
|
||||
language: null,
|
||||
value: 'Sample Community'
|
||||
}
|
||||
],
|
||||
'dc.identifier.uri': [
|
||||
{
|
||||
language: null,
|
||||
value: 'http://localhost:4000/handle/10673/1'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@@ -216,6 +216,7 @@ import { CommunitySidebarSearchListElementComponent } from './object-list/sideba
|
||||
import { AuthorizedCollectionSelectorComponent } from './dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component';
|
||||
import { DsoPageEditButtonComponent } from './dso-page/dso-page-edit-button/dso-page-edit-button.component';
|
||||
import { HoverClassDirective } from './hover-class.directive';
|
||||
import { ValidationSuggestionsComponent } from './input-suggestions/validation-suggestions/validation-suggestions.component';
|
||||
import { ItemSearchResultGridElementComponent } from './object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component';
|
||||
|
||||
const MODULES = [
|
||||
@@ -338,6 +339,7 @@ const COMPONENTS = [
|
||||
BrowseByComponent,
|
||||
InputSuggestionsComponent,
|
||||
FilterInputSuggestionsComponent,
|
||||
ValidationSuggestionsComponent,
|
||||
DsoInputSuggestionsComponent,
|
||||
DSOSelectorComponent,
|
||||
CreateCommunityParentSelectorComponent,
|
||||
|
@@ -12,6 +12,7 @@ export const GroupMock2: Group = Object.assign(new Group(), {
|
||||
href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2',
|
||||
},
|
||||
subgroups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/subgroups' },
|
||||
object: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/object' },
|
||||
epersons: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons' }
|
||||
},
|
||||
_name: 'testgroupname2',
|
||||
@@ -31,6 +32,7 @@ export const GroupMock: Group = Object.assign(new Group(), {
|
||||
href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid',
|
||||
},
|
||||
subgroups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/subgroups' },
|
||||
object: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/object' },
|
||||
epersons: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/epersons' }
|
||||
},
|
||||
_name: 'testgroupname',
|
||||
|
@@ -14,6 +14,7 @@ import { SharedModule } from '../../shared/shared.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
describe('CollectionStatisticsPageComponent', () => {
|
||||
|
||||
@@ -59,6 +60,11 @@ describe('CollectionStatisticsPageComponent', () => {
|
||||
getName: () => observableOf('test dso name'),
|
||||
};
|
||||
|
||||
const authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true),
|
||||
setRedirectUrl: {}
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
@@ -75,6 +81,7 @@ describe('CollectionStatisticsPageComponent', () => {
|
||||
{ provide: UsageReportService, useValue: usageReportService },
|
||||
{ provide: DSpaceObjectDataService, useValue: {} },
|
||||
{ provide: DSONameService, useValue: nameService },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
],
|
||||
})
|
||||
.compileComponents();
|
||||
|
@@ -4,6 +4,7 @@ import { UsageReportService } from '../../core/statistics/usage-report-data.serv
|
||||
import { ActivatedRoute , Router} from '@angular/router';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
/**
|
||||
* Component representing the statistics page for a collection.
|
||||
@@ -30,12 +31,14 @@ export class CollectionStatisticsPageComponent extends StatisticsPageComponent<C
|
||||
protected router: Router,
|
||||
protected usageReportService: UsageReportService,
|
||||
protected nameService: DSONameService,
|
||||
protected authService: AuthService
|
||||
) {
|
||||
super(
|
||||
route,
|
||||
router,
|
||||
usageReportService,
|
||||
nameService,
|
||||
authService,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ import { SharedModule } from '../../shared/shared.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
describe('CommunityStatisticsPageComponent', () => {
|
||||
|
||||
@@ -59,6 +60,11 @@ describe('CommunityStatisticsPageComponent', () => {
|
||||
getName: () => observableOf('test dso name'),
|
||||
};
|
||||
|
||||
const authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true),
|
||||
setRedirectUrl: {}
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
@@ -75,6 +81,7 @@ describe('CommunityStatisticsPageComponent', () => {
|
||||
{ provide: UsageReportService, useValue: usageReportService },
|
||||
{ provide: DSpaceObjectDataService, useValue: {} },
|
||||
{ provide: DSONameService, useValue: nameService },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
],
|
||||
})
|
||||
.compileComponents();
|
||||
|
@@ -4,6 +4,7 @@ import { UsageReportService } from '../../core/statistics/usage-report-data.serv
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
/**
|
||||
* Component representing the statistics page for a community.
|
||||
@@ -30,12 +31,14 @@ export class CommunityStatisticsPageComponent extends StatisticsPageComponent<Co
|
||||
protected router: Router,
|
||||
protected usageReportService: UsageReportService,
|
||||
protected nameService: DSONameService,
|
||||
protected authService: AuthService,
|
||||
) {
|
||||
super(
|
||||
route,
|
||||
router,
|
||||
usageReportService,
|
||||
nameService,
|
||||
authService,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ import { SharedModule } from '../../shared/shared.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
describe('ItemStatisticsPageComponent', () => {
|
||||
|
||||
@@ -59,6 +60,11 @@ describe('ItemStatisticsPageComponent', () => {
|
||||
getName: () => observableOf('test dso name'),
|
||||
};
|
||||
|
||||
const authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true),
|
||||
setRedirectUrl: {}
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
@@ -75,6 +81,7 @@ describe('ItemStatisticsPageComponent', () => {
|
||||
{ provide: UsageReportService, useValue: usageReportService },
|
||||
{ provide: DSpaceObjectDataService, useValue: {} },
|
||||
{ provide: DSONameService, useValue: nameService },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
],
|
||||
})
|
||||
.compileComponents();
|
||||
|
@@ -4,6 +4,7 @@ import { UsageReportService } from '../../core/statistics/usage-report-data.serv
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
/**
|
||||
* Component representing the statistics page for an item.
|
||||
@@ -31,12 +32,14 @@ export class ItemStatisticsPageComponent extends StatisticsPageComponent<Item> {
|
||||
protected router: Router,
|
||||
protected usageReportService: UsageReportService,
|
||||
protected nameService: DSONameService,
|
||||
protected authService: AuthService
|
||||
) {
|
||||
super(
|
||||
route,
|
||||
router,
|
||||
usageReportService,
|
||||
nameService,
|
||||
authService,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { SiteDataService } from '../../core/data/site-data.service';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
describe('SiteStatisticsPageComponent', () => {
|
||||
|
||||
@@ -55,6 +56,11 @@ describe('SiteStatisticsPageComponent', () => {
|
||||
}))
|
||||
};
|
||||
|
||||
const authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true),
|
||||
setRedirectUrl: {}
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
@@ -72,6 +78,7 @@ describe('SiteStatisticsPageComponent', () => {
|
||||
{ provide: DSpaceObjectDataService, useValue: {} },
|
||||
{ provide: DSONameService, useValue: nameService },
|
||||
{ provide: SiteDataService, useValue: siteService },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
],
|
||||
})
|
||||
.compileComponents();
|
||||
|
@@ -6,6 +6,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Site } from '../../core/shared/site.model';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
/**
|
||||
* Component representing the site-wide statistics page.
|
||||
@@ -30,12 +31,14 @@ export class SiteStatisticsPageComponent extends StatisticsPageComponent<Site> {
|
||||
protected usageReportService: UsageReportService,
|
||||
protected nameService: DSONameService,
|
||||
protected siteService: SiteDataService,
|
||||
protected authService: AuthService,
|
||||
) {
|
||||
super(
|
||||
route,
|
||||
router,
|
||||
usageReportService,
|
||||
nameService,
|
||||
authService,
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -4,10 +4,11 @@ import { UsageReportService } from '../../core/statistics/usage-report-data.serv
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { UsageReport } from '../../core/statistics/models/usage-report.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { getRemoteDataPayload, getSucceededRemoteData, redirectOn404Or401 } from '../../core/shared/operators';
|
||||
import { getRemoteDataPayload, getSucceededRemoteData, redirectOn4xx } from '../../core/shared/operators';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
/**
|
||||
* Class representing an abstract statistics page component.
|
||||
@@ -36,6 +37,7 @@ export abstract class StatisticsPageComponent<T extends DSpaceObject> implements
|
||||
protected router: Router,
|
||||
protected usageReportService: UsageReportService,
|
||||
protected nameService: DSONameService,
|
||||
protected authService: AuthService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -55,7 +57,7 @@ export abstract class StatisticsPageComponent<T extends DSpaceObject> implements
|
||||
protected getScope$(): Observable<DSpaceObject> {
|
||||
return this.route.data.pipe(
|
||||
map((data) => data.scope as RemoteData<T>),
|
||||
redirectOn404Or401(this.router),
|
||||
redirectOn4xx(this.router, this.authService),
|
||||
getSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
);
|
||||
|
@@ -1,10 +0,0 @@
|
||||
<div class="unauthorized container">
|
||||
<h1>401</h1>
|
||||
<h2><small>{{"401.unauthorized" | translate}}</small></h2>
|
||||
<br/>
|
||||
<p>{{"401.help" | translate}}</p>
|
||||
<br/>
|
||||
<p class="text-center">
|
||||
<a routerLink="/home" class="btn btn-primary">{{"401.link.home-page" | translate}}</a>
|
||||
</p>
|
||||
</div>
|
@@ -4210,9 +4210,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importing from LC Name",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importing from Sherpa Journal",
|
||||
// TODO New key - Add a translation
|
||||
@@ -4322,9 +4322,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})",
|
||||
// TODO New key - Add a translation
|
||||
@@ -4402,9 +4402,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -4078,9 +4078,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importing from LC Name",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importing from Sherpa Journal",
|
||||
// TODO New key - Add a translation
|
||||
@@ -4190,9 +4190,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})",
|
||||
// TODO New key - Add a translation
|
||||
@@ -4270,9 +4270,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -3437,8 +3437,8 @@
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importing from LC Name",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Von LC Name importieren",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Von ORCID importieren",
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Von ORCID importieren",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importing from Sherpa Journal",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Von Sherpa Zeitschriften importieren",
|
||||
@@ -3521,8 +3521,8 @@
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Verlage ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Namen ({{ count }})",
|
||||
@@ -3581,8 +3581,8 @@
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Suchergebnisse",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Suchergebnisse",
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Suchergebnisse",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Suchergebnisse",
|
||||
|
@@ -8,6 +8,14 @@
|
||||
|
||||
|
||||
|
||||
"403.help": "You don't have permission to access this page. You can use the button below to get back to the home page.",
|
||||
|
||||
"403.link.home-page": "Take me to the home page",
|
||||
|
||||
"403.forbidden": "forbidden",
|
||||
|
||||
|
||||
|
||||
"404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ",
|
||||
|
||||
"404.link.home-page": "Take me to the home page",
|
||||
@@ -280,6 +288,10 @@
|
||||
|
||||
"admin.access-control.groups.title": "DSpace Angular :: Groups",
|
||||
|
||||
"admin.access-control.groups.title.singleGroup": "DSpace Angular :: Edit Group",
|
||||
|
||||
"admin.access-control.groups.title.addGroup": "DSpace Angular :: New Group",
|
||||
|
||||
"admin.access-control.groups.head": "Groups",
|
||||
|
||||
"admin.access-control.groups.button.add": "Add group",
|
||||
@@ -308,8 +320,15 @@
|
||||
|
||||
"admin.access-control.groups.notification.deleted.success": "Successfully deleted group \"{{name}}\"",
|
||||
|
||||
"admin.access-control.groups.notification.deleted.failure": "Failed to delete group \"{{name}}\"",
|
||||
"admin.access-control.groups.notification.deleted.failure.title": "Failed to delete group \"{{name}}\"",
|
||||
|
||||
"admin.access-control.groups.notification.deleted.failure.content": "Cause: \"{{cause}}\"",
|
||||
|
||||
|
||||
|
||||
"admin.access-control.groups.form.alert.permanent": "This group is permanent, so it can't be edited or deleted. You can still add and remove group members using this page.",
|
||||
|
||||
"admin.access-control.groups.form.alert.workflowGroup": "This group can’t be modified or deleted because it corresponds to a role in the submission and workflow process in the \"{{name}}\" {{comcol}}. You can delete it from the <a href='{{comcolEditRolesRoute}}'>\"assign roles\"</a> tab on the edit {{comcol}} page. You can still add and remove group members using this page.",
|
||||
|
||||
"admin.access-control.groups.form.head.create": "Create group",
|
||||
|
||||
@@ -325,6 +344,28 @@
|
||||
|
||||
"admin.access-control.groups.form.notification.created.failure.groupNameInUse": "Failed to create Group with name: \"{{name}}\", make sure the name is not already in use.",
|
||||
|
||||
"admin.access-control.groups.form.notification.edited.failure": "Failed to edit Group \"{{name}}\"",
|
||||
|
||||
"admin.access-control.groups.form.notification.edited.failure.groupNameInUse": "Name \"{{name}}\" already in use!",
|
||||
|
||||
"admin.access-control.groups.form.notification.edited.success": "Successfully edited Group \"{{name}}\"",
|
||||
|
||||
"admin.access-control.groups.form.actions.delete": "Delete Group",
|
||||
|
||||
"admin.access-control.groups.form.delete-group.modal.header": "Delete Group \"{{ dsoName }}\"",
|
||||
|
||||
"admin.access-control.groups.form.delete-group.modal.info": "Are you sure you want to delete Group \"{{ dsoName }}\"",
|
||||
|
||||
"admin.access-control.groups.form.delete-group.modal.cancel": "Cancel",
|
||||
|
||||
"admin.access-control.groups.form.delete-group.modal.confirm": "Delete",
|
||||
|
||||
"admin.access-control.groups.form.notification.deleted.success": "Successfully deleted group \"{{ name }}\"",
|
||||
|
||||
"admin.access-control.groups.form.notification.deleted.failure.title": "Failed to delete group \"{{ name }}\"",
|
||||
|
||||
"admin.access-control.groups.form.notification.deleted.failure.content": "Cause: \"{{ cause }}\"",
|
||||
|
||||
"admin.access-control.groups.form.members-list.head": "EPeople",
|
||||
|
||||
"admin.access-control.groups.form.members-list.search.head": "Add EPeople",
|
||||
@@ -2324,7 +2365,11 @@
|
||||
|
||||
"process.detail.output" : "Process Output",
|
||||
|
||||
"process.detail.output.alert" : "Work in progress - Process output is not available yet",
|
||||
"process.detail.logs.button": "Retrieve process output",
|
||||
|
||||
"process.detail.logs.loading": "Retrieving",
|
||||
|
||||
"process.detail.logs.none": "This process has no output",
|
||||
|
||||
"process.detail.output-files" : "Output Files",
|
||||
|
||||
@@ -2998,7 +3043,7 @@
|
||||
|
||||
"submission.import-external.source.sherpaPublisher": "SHERPA Publishers",
|
||||
|
||||
"submission.import-external.source.orcidV2": "ORCID",
|
||||
"submission.import-external.source.orcid": "ORCID",
|
||||
|
||||
"submission.import-external.source.pubmed": "Pubmed",
|
||||
|
||||
@@ -3046,7 +3091,7 @@
|
||||
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importing from LC Name",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importing from Sherpa Journal",
|
||||
|
||||
@@ -3121,7 +3166,7 @@
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})",
|
||||
|
||||
@@ -3205,7 +3250,7 @@
|
||||
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcidv2": "Search Results",
|
||||
|
||||
|
@@ -3539,9 +3539,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importing from LC Name",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importing from Sherpa Journal",
|
||||
// TODO New key - Add a translation
|
||||
@@ -3651,9 +3651,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})",
|
||||
// TODO New key - Add a translation
|
||||
@@ -3731,9 +3731,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -3537,9 +3537,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importing from LC Name",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importing from Sherpa Journal",
|
||||
// TODO New key - Add a translation
|
||||
@@ -3649,9 +3649,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})",
|
||||
// TODO New key - Add a translation
|
||||
@@ -3729,9 +3729,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -3541,9 +3541,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importing from LC Name",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importing from Sherpa Journal",
|
||||
// TODO New key - Add a translation
|
||||
@@ -3653,9 +3653,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})",
|
||||
// TODO New key - Add a translation
|
||||
@@ -3733,9 +3733,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -4210,9 +4210,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importing from LC Name",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importing from Sherpa Journal",
|
||||
// TODO New key - Add a translation
|
||||
@@ -4322,9 +4322,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})",
|
||||
// TODO New key - Add a translation
|
||||
@@ -4402,9 +4402,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -3340,8 +3340,8 @@
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importing from LC Name",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importē no LC Nosaukuma",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importē no ORCID",
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importē no ORCID",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importing from Sherpa Journal",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importē no Sherpa Žurnāla",
|
||||
@@ -3424,8 +3424,8 @@
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Izdevēji ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Nosaukumi ({{ count }})",
|
||||
@@ -3484,8 +3484,8 @@
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Meklēšanas rezultāti",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Meklēšanas rezultāti",
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Meklēšanas rezultāti",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Meklēšanas rezultāti",
|
||||
|
@@ -3536,9 +3536,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importing from LC Name",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importing from Sherpa Journal",
|
||||
// TODO New key - Add a translation
|
||||
@@ -3648,9 +3648,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})",
|
||||
// TODO New key - Add a translation
|
||||
@@ -3728,9 +3728,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results",
|
||||
// TODO New key - Add a translation
|
||||
|
@@ -4210,9 +4210,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.lcname": "Importing from LC Name",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcidV2": "Importing from ORCID",
|
||||
"submission.sections.describe.relationship-lookup.external-source.import-modal.head.orcid": "Importing from ORCID",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.external-source.import-modal.head.sherpaJournal": "Importing from Sherpa Journal",
|
||||
// TODO New key - Add a translation
|
||||
@@ -4322,9 +4322,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcid": "ORCID ({{ count }})",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})",
|
||||
// TODO New key - Add a translation
|
||||
@@ -4402,9 +4402,9 @@
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
// TODO New key - Add a translation
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcid": "Search Results",
|
||||
|
||||
// "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results",
|
||||
// TODO New key - Add a translation
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user