Merge branch 'main' into #885-media-viewer

# Conflicts:
#	src/app/+item-page/item-page.module.ts
#	src/app/+item-page/simple/item-types/shared/item.component.ts
#	src/app/app.component.scss
#	src/environments/mock-environment.ts
This commit is contained in:
Dániel Péter Sipos
2021-03-26 16:57:07 +01:00
672 changed files with 54289 additions and 13976 deletions

View File

@@ -19,7 +19,7 @@ jobs:
strategy:
# Create a matrix of Node versions to test against (in parallel)
matrix:
node-version: [10.x, 12.x]
node-version: [12.x, 14.x]
# Do NOT exit immediately if one matrix job fails
fail-fast: false
# These are the actual CI steps to perform per job
@@ -80,6 +80,19 @@ jobs:
docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
docker container ls
# Wait until the REST API returns a 200 response (or for a max of 30 seconds)
# https://github.com/nev7n/wait_for_response
- name: Wait for DSpace REST Backend to be ready (for e2e tests)
uses: nev7n/wait_for_response@v1
with:
# We use the 'sites' endpoint to also ensure the database is ready
url: 'http://localhost:8080/server/api/core/sites'
responseCode: 200
timeout: 30000
- name: Get DSpace REST Backend info/properties
run: curl http://localhost:8080/server/api
- name: Run e2e tests (integration tests)
run: yarn run e2e:ci

View File

@@ -13,7 +13,7 @@ You can find additional information on the DSpace 7 Angular UI on the [wiki](htt
Quick start
-----------
**Ensure you're running [Node](https://nodejs.org) `v10.x` or `v12.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) >= `v1.x`**
**Ensure you're running [Node](https://nodejs.org) `v12.x` or `v14.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) >= `v1.x`**
```bash
# clone the repo
@@ -65,7 +65,7 @@ Requirements
------------
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
- Ensure you're running node `v10.x` or `v12.x` and yarn >= `v1.x`
- Ensure you're running node `v12.x` or `v14.x` and yarn >= `v1.x`
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
@@ -339,7 +339,6 @@ dspace-angular
├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration
├── typedoc.json * TYPEDOC configuration
├── webpack * Webpack (https://webpack.github.io/) config directory
│   ├── helpers.js *
│   ├── webpack.aot.js * Webpack (https://webpack.github.io/) config for AoT build
│   ├── webpack.client.js * Webpack (https://webpack.github.io/) config for client build
│   ├── webpack.common.js *

View File

@@ -17,6 +17,7 @@
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"extractCss": true,
"preserveSymlinks": true,
"customWebpackConfig": {
"path": "./webpack/webpack.browser.ts",
@@ -46,7 +47,16 @@
"src/robots.txt"
],
"styles": [
"src/styles.scss"
{
"input": "src/styles/base-theme.scss",
"inject": false,
"bundleName": "base-theme"
},
{
"input": "src/themes/custom/styles/theme.scss",
"inject": false,
"bundleName": "custom-theme"
}
],
"scripts": []
},
@@ -116,7 +126,11 @@
"src/assets"
],
"styles": [
"src/styles.scss"
{
"input": "src/styles/base-theme.scss",
"inject": false,
"bundleName": "base-theme"
}
],
"scripts": []
}

View File

@@ -20,16 +20,17 @@
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
"start:dev": "npm-run-all --parallel config:dev:watch serve",
"start:prod": "yarn run build:prod && yarn run serve:ssr",
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
"build": "ng build",
"build:stats": "ng build --stats-json",
"build:prod": "yarn run build:ssr",
"build:ssr": "yarn run build:client-and-server-bundles && yarn run compile:server",
"ng-high-memory": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng",
"build:client-and-server-bundles": "npm run ng-high-memory -- build --prod && npm run ng-high-memory -- run dspace-angular:server:production --bundleDependencies true",
"build:client-and-server-bundles": "ng build --prod && ng run dspace-angular:server:production --bundleDependencies true",
"test:watch": "npm-run-all --parallel config:test:watch test",
"test": "npm run ng-high-memory -- test --sourceMap=true --watch=true",
"test:headless": "npm run ng-high-memory -- test --watch=false --sourceMap=true --browsers=ChromeHeadless --code-coverage",
"test": "ng test --sourceMap=true --watch=true",
"test:headless": "ng test --watch=false --sourceMap=true --browsers=ChromeHeadless --code-coverage",
"lint": "ng lint",
"lint-fix": "npm run ng-high-memory -- lint --fix=true",
"lint-fix": "ng lint --fix=true",
"e2e": "ng e2e",
"e2e:ci": "ng e2e --protractor-config=./e2e/protractor-ci.conf.js",
"compile:server": "webpack --config webpack.server.config.js --progress --color",
@@ -144,7 +145,7 @@
"@types/node": "^14.14.9",
"codelyzer": "^6.0.1",
"compression-webpack-plugin": "^3.0.1",
"copy-webpack-plugin": "^5.1.1",
"copy-webpack-plugin": "^6.4.1",
"css-loader": "3.4.0",
"cssnano": "^4.1.10",
"deep-freeze": "0.0.1",
@@ -181,7 +182,7 @@
"tslint": "^6.1.3",
"typescript": "~4.0.5",
"webpack": "^4.44.2",
"webpack-bundle-analyzer": "^3.3.2",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.2.0",
"webpack-node-externals": "1.7.2"
}

View File

@@ -1,7 +1,7 @@
module.exports = {
plugins: [
require('postcss-import')(),
require('postcss-cssnext')(),
require('postcss-preset-env')(),
require('postcss-apply')(),
require('postcss-responsive-type')()
]

View File

@@ -10,7 +10,7 @@ const targetPath = './src/environments/environment.ts';
const colors = require('colors');
require('dotenv').config();
const merge = require('deepmerge');
const mergeOptions = { arrayMerge: (destinationArray, sourceArray, options) => sourceArray };
const environment = process.argv[2];
let environmentFilePath;
let production = false;
@@ -45,10 +45,10 @@ const processEnv = {
} as GlobalConfig;
import(environmentFilePath)
.then((file) => generateEnvironmentFile(merge.all([commonEnv, file.environment, processEnv])))
.then((file) => generateEnvironmentFile(merge.all([commonEnv, file.environment, processEnv], mergeOptions)))
.catch(() => {
console.log(colors.yellow.bold(`No specific environment file found for ` + environment));
generateEnvironmentFile(merge(commonEnv, processEnv))
generateEnvironmentFile(merge(commonEnv, processEnv, mergeOptions))
});
function generateEnvironmentFile(file: GlobalConfig): void {

View File

@@ -1,22 +0,0 @@
const syncBuildDir = require('copyfiles');
const path = require('path');
const {
projectRoot,
theme,
themePath,
} = require('../webpack/helpers');
const projectDepth = projectRoot('./').split(path.sep).length;
let callback;
if (theme !== null && theme !== undefined) {
callback = () => {
syncBuildDir([path.join(themePath, '**/*'), 'build'], { up: projectDepth + 2 }, () => {})
}
}
else {
callback = () => {};
}
syncBuildDir([projectRoot('src/**/*'), 'build'], { up: projectDepth + 1 }, callback);

View File

@@ -143,7 +143,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
initialisePage() {
this.subs.push(this.route.params.subscribe((params) => {
if (params.groupId !== 'newGroup') {
this.setActiveGroup(params.groupId);
}
}));
this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
hasValueOperator(),
@@ -225,14 +227,12 @@ export class GroupFormComponent implements OnInit, OnDestroy {
{
value: this.groupDescription.value
}
],
]
},
};
if (group === null) {
console.log('createNewGroup', values);
this.createNewGroup(values);
} else {
console.log('editGroup', group);
this.editGroup(group);
}
}

View File

@@ -24,10 +24,10 @@
</div>
</form>
<ds-pagination *ngIf="(searchResults$ | async)?.payload?.totalElements > 0"
<ds-pagination *ngIf="(ePeopleSearchDtos | async)?.totalElements > 0"
[paginationOptions]="configSearch"
[pageInfoState]="(searchResults$ | async)?.payload"
[collectionSize]="(searchResults$ | async)?.payload?.totalElements"
[pageInfoState]="(ePeopleSearchDtos | async)"
[collectionSize]="(ePeopleSearchDtos | async)?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(pageChange)="onPageChangeSearch($event)">
@@ -42,23 +42,23 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let ePerson of (searchResults$ | async)?.payload?.page">
<td>{{ePerson.id}}</td>
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson)"
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.name}}</a></td>
<tr *ngFor="let ePerson of (ePeopleSearchDtos | async)?.page">
<td>{{ePerson.eperson.id}}</td>
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.eperson.name}}</a></td>
<td>
<div class="btn-group edit-field">
<button *ngIf="(isMemberOfGroup(ePerson) | async)"
<button *ngIf="(ePerson.memberOfGroup)"
(click)="deleteMemberFromGroup(ePerson)"
class="btn btn-outline-danger btn-sm"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.name} }}">
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.eperson.name} }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
<button *ngIf="!(isMemberOfGroup(ePerson) | async)"
<button *ngIf="!(ePerson.memberOfGroup)"
(click)="addMemberToGroup(ePerson)"
class="btn btn-outline-primary btn-sm"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: ePerson.name} }}">
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: ePerson.eperson.name} }}">
<i class="fas fa-plus fa-fw"></i>
</button>
</div>
@@ -70,7 +70,7 @@
</ds-pagination>
<div *ngIf="(searchResults$ | async)?.payload?.totalElements == 0 && searchDone"
<div *ngIf="(ePeopleSearchDtos | async)?.totalElements == 0 && searchDone"
class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-items' | translate}}
@@ -78,10 +78,10 @@
<h4>{{messagePrefix + '.headMembers' | translate}}</h4>
<ds-pagination *ngIf="(members$ | async)?.payload?.totalElements > 0"
<ds-pagination *ngIf="(ePeopleMembersOfGroupDtos | async)?.totalElements > 0"
[paginationOptions]="config"
[pageInfoState]="(members$ | async)?.payload"
[collectionSize]="(members$ | async)?.payload?.totalElements"
[pageInfoState]="(ePeopleMembersOfGroupDtos | async)"
[collectionSize]="(ePeopleMembersOfGroupDtos | async)?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(pageChange)="onPageChange($event)">
@@ -96,15 +96,15 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let ePerson of (members$ | async)?.payload?.page">
<td>{{ePerson.id}}</td>
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson)"
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.name}}</a></td>
<tr *ngFor="let ePerson of (ePeopleMembersOfGroupDtos | async)?.page">
<td>{{ePerson.eperson.id}}</td>
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.eperson.name}}</a></td>
<td>
<div class="btn-group edit-field">
<button (click)="deleteMemberFromGroup(ePerson)"
class="btn btn-outline-danger btn-sm"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.name} }}">
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.eperson.name} }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
</div>
@@ -116,7 +116,7 @@
</ds-pagination>
<div *ngIf="(members$ | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2"
<div *ngIf="(ePeopleMembersOfGroupDtos | async) == undefined || (ePeopleMembersOfGroupDtos | async)?.totalElements == 0" class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-members-yet' | translate}}
</div>

View File

@@ -2,9 +2,16 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Observable, of as observableOf, Subscription, BehaviorSubject } from 'rxjs';
import { map, mergeMap, take } from 'rxjs/operators';
import { PaginatedList } from '../../../../../core/data/paginated-list.model';
import {
Observable,
of as observableOf,
Subscription,
BehaviorSubject,
combineLatest as observableCombineLatest,
ObservedValueOf,
} from 'rxjs';
import { map, mergeMap, switchMap, take } from 'rxjs/operators';
import { buildPaginatedList, PaginatedList } from '../../../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../../core/data/remote-data';
import { EPersonDataService } from '../../../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../../../core/eperson/group-data.service';
@@ -13,18 +20,20 @@ import { Group } from '../../../../../core/eperson/models/group.model';
import {
getRemoteDataPayload,
getFirstSucceededRemoteData,
getFirstCompletedRemoteData
getFirstCompletedRemoteData,
getAllCompletedRemoteData
} from '../../../../../core/shared/operators';
import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model';
import { EpersonDtoModel } from '../../../../../core/eperson/models/eperson-dto.model';
/**
* Keys to keep track of specific subscriptions
*/
enum SubKey {
Members,
ActiveGroup,
SearchResults,
MembersDTO,
SearchResultsDTO,
}
@Component({
@@ -42,11 +51,11 @@ export class MembersListComponent implements OnInit, OnDestroy {
/**
* EPeople being displayed in search result, initially all members, after search result of search
*/
searchResults$: BehaviorSubject<RemoteData<PaginatedList<EPerson>>> = new BehaviorSubject(undefined);
ePeopleSearchDtos: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>(undefined);
/**
* List of EPeople members of currently active group being edited
*/
members$: BehaviorSubject<RemoteData<PaginatedList<EPerson>>> = new BehaviorSubject(undefined);
ePeopleMembersOfGroupDtos: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>(undefined);
/**
* Pagination config used to display the list of EPeople that are result of EPeople search
@@ -130,15 +139,59 @@ export class MembersListComponent implements OnInit, OnDestroy {
* @private
*/
private retrieveMembers(page: number) {
this.unsubFrom(SubKey.Members);
this.subs.set(
SubKey.Members,
this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, {
this.unsubFrom(SubKey.MembersDTO);
this.subs.set(SubKey.MembersDTO, this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, {
currentPage: page,
elementsPerPage: this.config.pageSize
}).pipe(
getAllCompletedRemoteData(),
map((rd: RemoteData<any>) => {
if (rd.hasFailed) {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', {cause: rd.errorMessage}));
} else {
return rd;
}
}),
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
const dtos$ = observableCombineLatest(...epersonListRD.payload.page.map((member: EPerson) => {
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
epersonDtoModel.eperson = member;
epersonDtoModel.memberOfGroup = isMember;
return epersonDtoModel;
});
return dto$;
}));
return dtos$.pipe(map((dtos: EpersonDtoModel[]) => {
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
}));
}))
.subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
this.ePeopleMembersOfGroupDtos.next(paginatedListOfDTOs);
}));
}
/**
* Whether or not the given ePerson is a member of the group currently being edited
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
*/
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
return this.groupDataService.getActiveGroup().pipe(take(1),
mergeMap((group: Group) => {
if (group != null) {
return this.ePersonDataService.findAllByHref(group._links.epersons.href, {
currentPage: 1,
elementsPerPage: 9999
}, false)
.pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
map((listEPeopleInGroup: PaginatedList<EPerson>) => listEPeopleInGroup.page.filter((ePersonInList: EPerson) => ePersonInList.id === possibleMember.id)),
map((epeople: EPerson[]) => epeople.length > 0));
} else {
return observableOf(false);
}
).subscribe((rd: RemoteData<PaginatedList<EPerson>>) => {
this.members$.next(rd);
}));
}
@@ -160,11 +213,12 @@ export class MembersListComponent implements OnInit, OnDestroy {
* Deletes a given EPerson from the members list of the group currently being edited
* @param ePerson EPerson we want to delete as member from group that is currently being edited
*/
deleteMemberFromGroup(ePerson: EPerson) {
deleteMemberFromGroup(ePerson: EpersonDtoModel) {
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
if (activeGroup != null) {
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson);
this.showNotifications('deleteMember', response, ePerson.name, activeGroup);
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
this.showNotifications('deleteMember', response, ePerson.eperson.name, activeGroup);
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
}
@@ -175,40 +229,18 @@ export class MembersListComponent implements OnInit, OnDestroy {
* Adds a given EPerson to the members list of the group currently being edited
* @param ePerson EPerson we want to add as member to group that is currently being edited
*/
addMemberToGroup(ePerson: EPerson) {
addMemberToGroup(ePerson: EpersonDtoModel) {
ePerson.memberOfGroup = true;
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
if (activeGroup != null) {
const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson);
this.showNotifications('addMember', response, ePerson.name, activeGroup);
const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson.eperson);
this.showNotifications('addMember', response, ePerson.eperson.name, activeGroup);
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
}
});
}
/**
* Whether or not the given ePerson is a member of the group currently being edited
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
*/
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
return this.groupDataService.getActiveGroup().pipe(take(1),
mergeMap((group: Group) => {
if (group != null) {
return this.ePersonDataService.findAllByHref(group._links.epersons.href, {
currentPage: 1,
elementsPerPage: 9999
})
.pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
map((listEPeopleInGroup: PaginatedList<EPerson>) => listEPeopleInGroup.page.filter((ePersonInList: EPerson) => ePersonInList.id === possibleMember.id)),
map((epeople: EPerson[]) => epeople.length > 0));
} else {
return observableOf(false);
}
}));
}
/**
* Search in the EPeople by name, email or metadata
* @param data Contains scope and query param
@@ -228,12 +260,37 @@ export class MembersListComponent implements OnInit, OnDestroy {
}
this.searchDone = true;
this.unsubFrom(SubKey.SearchResults);
this.subs.set(SubKey.SearchResults, this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
this.unsubFrom(SubKey.SearchResultsDTO);
this.subs.set(SubKey.SearchResultsDTO,
this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
currentPage: this.configSearch.currentPage,
elementsPerPage: this.configSearch.pageSize
}).subscribe((rd: RemoteData<PaginatedList<EPerson>>) => {
this.searchResults$.next(rd);
}, false).pipe(
getAllCompletedRemoteData(),
map((rd: RemoteData<any>) => {
if (rd.hasFailed) {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', {cause: rd.errorMessage}));
} else {
return rd;
}
}),
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
const dtos$ = observableCombineLatest(...epersonListRD.payload.page.map((member: EPerson) => {
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
epersonDtoModel.eperson = member;
epersonDtoModel.memberOfGroup = isMember;
return epersonDtoModel;
});
return dto$;
}));
return dtos$.pipe(map((dtos: EpersonDtoModel[]) => {
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
}));
}))
.subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
this.ePeopleSearchDtos.next(paginatedListOfDTOs);
}));
}

View File

@@ -8,7 +8,7 @@
<button class="mr-auto btn btn-success"
[routerLink]="['newGroup']">
<i class="fas fa-plus"></i>
<span class="d-none d-sm-inline">&nbsp;{{messagePrefix + 'button.add' | translate}}</span>
<span class="d-none d-sm-inline">{{messagePrefix + 'button.add' | translate}}</span>
</button>
</div>
@@ -45,7 +45,6 @@
<th scope="col">{{messagePrefix + 'table.id' | translate}}</th>
<th scope="col">{{messagePrefix + 'table.name' | translate}}</th>
<th scope="col">{{messagePrefix + 'table.members' | translate}}</th>
<!-- <th scope="col">{{messagePrefix + 'table.comcol' | translate}}</th>-->
<th>{{messagePrefix + 'table.edit' | translate}}</th>
</tr>
</thead>
@@ -53,8 +52,7 @@
<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>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td>
<td>
<div class="btn-group edit-field">
<button [routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
@@ -63,7 +61,7 @@
<i class="fas fa-edit fa-fw"></i>
</button>
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
(click)="deleteGroup(groupDto.group)" class="btn btn-outline-danger btn-sm"
(click)="deleteGroup(groupDto)" 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>

View File

@@ -26,7 +26,7 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import {
getAllSucceededRemoteDataPayload,
getFirstCompletedRemoteData,
getAllSucceededRemoteData
getFirstSucceededRemoteData
} from '../../../core/shared/operators';
import { PageInfo } from '../../../core/shared/page-info.model';
import { hasValue } from '../../../shared/empty.util';
@@ -55,15 +55,12 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
currentPage: 1
});
/**
* A list of all the current Groups within the repository or the result of the search
*/
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);
deletedGroupsIds: string[] = [];
/**
* An observable for the pageInfo, needed to pass to the pagination component
@@ -104,30 +101,6 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
ngOnInit() {
this.search({ query: this.currentSearchQuery });
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)
]).pipe(
map(([isAuthorized, hasLinkedDSO]: boolean[]) => {
const groupDtoModel: GroupDtoModel = new GroupDtoModel();
groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO;
groupDtoModel.group = group;
return groupDtoModel;
}
)
);
})).pipe(map((dtos: GroupDtoModel[]) => {
return buildPaginatedList(groups.pageInfo, dtos);
}));
})).subscribe((value: PaginatedList<GroupDtoModel>) => {
this.groupsDto$.next(value);
this.pageInfoState$.next(value.pageInfo);
}));
}
/**
@@ -154,14 +127,42 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
this.searchSub.unsubscribe();
this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub);
}
this.searchSub = this.groupService.searchGroups(this.currentSearchQuery.trim(), {
currentPage: this.config.currentPage,
elementsPerPage: this.config.pageSize
}).pipe(
getAllSucceededRemoteData()
).subscribe((groupsRD: RemoteData<PaginatedList<Group>>) => {
this.groups$.next(groupsRD);
this.pageInfoState$.next(groupsRD.payload.pageInfo);
getAllSucceededRemoteDataPayload(),
switchMap((groups: PaginatedList<Group>) => {
if (groups.page.length === 0) {
return observableOf(buildPaginatedList(groups.pageInfo, []));
}
return observableCombineLatest(groups.page.map((group: Group) => {
if (!this.deletedGroupsIds.includes(group.id)) {
return observableCombineLatest([
this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined),
this.hasLinkedDSO(group),
this.getSubgroups(group),
this.getMembers(group)
]).pipe(
map(([isAuthorized, hasLinkedDSO, subgroups, members]:
[boolean, boolean, RemoteData<PaginatedList<Group>>, RemoteData<PaginatedList<EPerson>>]) => {
const groupDtoModel: GroupDtoModel = new GroupDtoModel();
groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO;
groupDtoModel.group = group;
groupDtoModel.subgroups = subgroups.payload;
groupDtoModel.epersons = members.payload;
return groupDtoModel;
}
)
);
}
})).pipe(map((dtos: GroupDtoModel[]) => {
return buildPaginatedList(groups.pageInfo, dtos);
}));
})).subscribe((value: PaginatedList<GroupDtoModel>) => {
this.groupsDto$.next(value);
this.pageInfoState$.next(value.pageInfo);
});
this.subs.push(this.searchSub);
}
@@ -169,16 +170,17 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
/**
* Delete Group
*/
deleteGroup(group: Group) {
if (hasValue(group.id)) {
this.groupService.delete(group.id).pipe(getFirstCompletedRemoteData())
deleteGroup(group: GroupDtoModel) {
if (hasValue(group.group.id)) {
this.groupService.delete(group.group.id).pipe(getFirstCompletedRemoteData())
.subscribe((rd: RemoteData<NoContent>) => {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.name }));
this.deletedGroupsIds = [...this.deletedGroupsIds, group.group.id];
this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.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.title', { name: group.group.name }),
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.content', { cause: rd.errorMessage }));
}
});
@@ -201,7 +203,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
* @param group
*/
getMembers(group: Group): Observable<RemoteData<PaginatedList<EPerson>>> {
return this.ePersonDataService.findAllByHref(group._links.epersons.href);
return this.ePersonDataService.findAllByHref(group._links.epersons.href).pipe(getFirstSucceededRemoteData());
}
/**
@@ -209,7 +211,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
* @param group
*/
getSubgroups(group: Group): Observable<RemoteData<PaginatedList<Group>>> {
return this.groupService.findAllByHref(group._links.subgroups.href);
return this.groupService.findAllByHref(group._links.subgroups.href).pipe(getFirstSucceededRemoteData());
}
/**
@@ -218,6 +220,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
*/
hasLinkedDSO(group: Group): Observable<boolean> {
return this.dSpaceObjectDataService.findByHref(group._links.object.href).pipe(
getFirstSucceededRemoteData(),
map((rd: RemoteData<DSpaceObject>) => hasValue(rd) && hasValue(rd.payload)),
catchError(() => observableOf(false)),
);
@@ -233,15 +236,6 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
this.search({ query: '' });
}
/**
* Extract optional UUID from a group name => To be resolved to community or collection with link
* (Or will be resolved in backend and added to group object, tbd) //TODO
* @param groupName
*/
getOptionalComColFromName(groupName: string): string {
return this.groupService.getUUIDFromString(groupName);
}
/**
* Unsub all subscriptions
*/

View File

@@ -16,6 +16,8 @@ import { RouterTestingModule } from '@angular/router/testing';
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-result-grid-element.component';
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
describe('ItemAdminSearchResultGridElementComponent', () => {
let component: ItemAdminSearchResultGridElementComponent;
@@ -29,6 +31,8 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
}
};
const mockThemeService = getMockThemeService();
function init() {
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
searchResult = new ItemSearchResult();
@@ -50,6 +54,7 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
providers: [
{ provide: TruncatableService, useValue: mockTruncatableService },
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
{ provide: ThemeService, useValue: mockThemeService },
],
schemas: [NO_ERRORS_SCHEMA]
})

View File

@@ -12,6 +12,7 @@ import { TruncatableService } from '../../../../../shared/truncatable/truncatabl
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
@listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminSearch)
@Component({
@@ -29,6 +30,7 @@ export class ItemAdminSearchResultGridElementComponent extends SearchResultGridE
constructor(protected truncatableService: TruncatableService,
protected bitstreamDataService: BitstreamDataService,
private themeService: ThemeService,
private componentFactoryResolver: ComponentFactoryResolver
) {
super(truncatableService, bitstreamDataService);
@@ -63,6 +65,6 @@ export class ItemAdminSearchResultGridElementComponent extends SearchResultGridE
* @returns {GenericConstructor<Component>}
*/
private getComponent(): GenericConstructor<Component> {
return getListableObjectComponent(this.object.getRenderTypes(), ViewMode.GridElement, undefined);
return getListableObjectComponent(this.object.getRenderTypes(), ViewMode.GridElement, undefined, this.themeService.getThemeName());
}
}

View File

@@ -11,6 +11,8 @@ import { Collection } from '../../../../../core/shared/collection.model';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
describe('CollectionAdminSearchResultListElementComponent', () => {
let component: CollectionAdminSearchResultListElementComponent;
@@ -33,7 +35,8 @@ describe('CollectionAdminSearchResultListElementComponent', () => {
RouterTestingModule.withRoutes([])
],
declarations: [CollectionAdminSearchResultListElementComponent],
providers: [{ provide: TruncatableService, useValue: {} }],
providers: [{ provide: TruncatableService, useValue: {} },
{ provide: DSONameService, useClass: DSONameServiceMock }],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();

View File

@@ -11,6 +11,8 @@ import { CommunityAdminSearchResultListElementComponent } from './community-admi
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
import { Community } from '../../../../../core/shared/community.model';
import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
describe('CommunityAdminSearchResultListElementComponent', () => {
let component: CommunityAdminSearchResultListElementComponent;
@@ -33,7 +35,8 @@ describe('CommunityAdminSearchResultListElementComponent', () => {
RouterTestingModule.withRoutes([])
],
declarations: [CommunityAdminSearchResultListElementComponent],
providers: [{ provide: TruncatableService, useValue: {} }],
providers: [{ provide: TruncatableService, useValue: {} },
{ provide: DSONameService, useClass: DSONameServiceMock }],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();

View File

@@ -8,6 +8,8 @@ import { RouterTestingModule } from '@angular/router/testing';
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
import { ItemAdminSearchResultListElementComponent } from './item-admin-search-result-list-element.component';
import { Item } from '../../../../../core/shared/item.model';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
describe('ItemAdminSearchResultListElementComponent', () => {
let component: ItemAdminSearchResultListElementComponent;
@@ -30,7 +32,8 @@ describe('ItemAdminSearchResultListElementComponent', () => {
RouterTestingModule.withRoutes([])
],
declarations: [ItemAdminSearchResultListElementComponent],
providers: [{ provide: TruncatableService, useValue: {} }],
providers: [{ provide: TruncatableService, useValue: {} },
{ provide: DSONameService, useClass: DSONameServiceMock }],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();

View File

@@ -56,19 +56,19 @@ describe('ItemAdminSearchResultActionsComponent', () => {
it('should render an edit button with the correct link', () => {
const button = fixture.debugElement.query(By.css('a.edit-link'));
const link = button.nativeElement.href;
expect(link).toContain(getItemEditRoute(id));
expect(link).toContain(getItemEditRoute(item));
});
it('should render a delete button with the correct link', () => {
const button = fixture.debugElement.query(By.css('a.delete-link'));
const link = button.nativeElement.href;
expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_DELETE_PATH).toString());
expect(link).toContain(new URLCombiner(getItemEditRoute(item), ITEM_EDIT_DELETE_PATH).toString());
});
it('should render a move button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.move-link'));
const link = a.nativeElement.href;
expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_MOVE_PATH).toString());
expect(link).toContain(new URLCombiner(getItemEditRoute(item), ITEM_EDIT_MOVE_PATH).toString());
});
describe('when the item is not withdrawn', () => {
@@ -80,7 +80,7 @@ describe('ItemAdminSearchResultActionsComponent', () => {
it('should render a withdraw button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.withdraw-link'));
const link = a.nativeElement.href;
expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_WITHDRAW_PATH).toString());
expect(link).toContain(new URLCombiner(getItemEditRoute(item), ITEM_EDIT_WITHDRAW_PATH).toString());
});
it('should not render a reinstate button with the correct link', () => {
@@ -103,7 +103,7 @@ describe('ItemAdminSearchResultActionsComponent', () => {
it('should render a reinstate button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.reinstate-link'));
const link = a.nativeElement.href;
expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_REINSTATE_PATH).toString());
expect(link).toContain(new URLCombiner(getItemEditRoute(item), ITEM_EDIT_REINSTATE_PATH).toString());
});
});
@@ -116,7 +116,7 @@ describe('ItemAdminSearchResultActionsComponent', () => {
it('should render a make private button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.private-link'));
const link = a.nativeElement.href;
expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_PRIVATE_PATH).toString());
expect(link).toContain(new URLCombiner(getItemEditRoute(item), ITEM_EDIT_PRIVATE_PATH).toString());
});
it('should not render a make public button with the correct link', () => {
@@ -139,7 +139,7 @@ describe('ItemAdminSearchResultActionsComponent', () => {
it('should render a make private button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.public-link'));
const link = a.nativeElement.href;
expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_PUBLIC_PATH).toString());
expect(link).toContain(new URLCombiner(getItemEditRoute(item), ITEM_EDIT_PUBLIC_PATH).toString());
});
});
});

View File

@@ -34,7 +34,7 @@ export class ItemAdminSearchResultActionsComponent {
* Returns the path to the edit page of this item
*/
getEditRoute(): string {
return getItemEditRoute(this.item.uuid);
return getItemEditRoute(this.item);
}
/**

View File

@@ -1,12 +1,12 @@
$icon-z-index: 10;
:host {
--ds-icon-z-index: 10;
left: 0;
top: 0;
height: 100vh;
flex: 1 1 auto;
nav {
background-color: $admin-sidebar-bg;
background-color: var(--ds-admin-sidebar-bg);
height: 100%;
flex-direction: column;
> div {
@@ -19,12 +19,12 @@ $icon-z-index: 10;
}
&.inactive ::ng-deep .sidebar-collapsible {
margin-left: -#{$sidebar-items-width};
margin-left: calc(-1 * var(--ds-sidebar-items-width));
}
.navbar-nav {
.admin-menu-header {
background-color: $admin-sidebar-header-bg;
background-color: var(--ds-admin-sidebar-header-bg);
.logo-wrapper {
img {
height: 20px;
@@ -43,29 +43,29 @@ $icon-z-index: 10;
.sidebar-section {
display: flex;
align-content: stretch;
background-color: $admin-sidebar-bg;
background-color: var(--ds-admin-sidebar-bg);
.nav-item {
padding-top: $spacer;
padding-bottom: $spacer;
padding-top: var(--bs-spacer);
padding-bottom: var(--bs-spacer);
}
.shortcut-icon {
padding-left: $icon-padding;
padding-right: $icon-padding;
padding-left: var(--ds-icon-padding);
padding-right: var(--ds-icon-padding);
}
.shortcut-icon, .icon-wrapper {
background-color: inherit;
z-index: $icon-z-index;
z-index: var(--ds-icon-z-index);
}
.sidebar-collapsible {
width: $sidebar-items-width;
width: var(--ds-sidebar-items-width);
position: relative;
a {
padding-right: $spacer;
padding-right: var(--bs-spacer);
width: 100%;
}
}
&.active > .sidebar-collapsible > .nav-link {
color: $navbar-dark-active-color;
color: var(--bs-navbar-dark-active-color);
}
}
}

View File

@@ -1,13 +1,13 @@
:host ::ng-deep {
.fa-chevron-right {
padding-left: $spacer/2;
padding-left: calc(var(--bs-spacer) / 2);
font-size: 0.5rem;
line-height: 3;
}
.sidebar-sub-level-items {
list-style: disc;
color: $navbar-dark-color;
color: var(--bs-navbar-dark-color);
overflow: hidden;
}

View File

@@ -19,6 +19,8 @@ import { BitstreamDataService } from '../../../../../core/data/bitstream-data.se
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
import { of as observableOf } from 'rxjs';
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
describe('WorkflowItemAdminWorkflowGridElementComponent', () => {
let component: WorkflowItemSearchResultAdminWorkflowGridElementComponent;
@@ -28,6 +30,7 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => {
let itemRD$;
let linkService;
let object;
let themeService;
function init() {
itemRD$ = createSuccessfulRemoteDataObject$(new Item());
@@ -37,6 +40,7 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => {
wfi.item = itemRD$;
object.indexableObject = wfi;
linkService = getMockLinkService();
themeService = getMockThemeService();
}
beforeEach(waitForAsync(() => {
@@ -51,6 +55,7 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => {
],
providers: [
{ provide: LinkService, useValue: linkService },
{ provide: ThemeService, useValue: themeService },
{
provide: TruncatableService, useValue: {
isCollapsed: () => observableOf(true),

View File

@@ -1,7 +1,10 @@
import { Component, ComponentFactoryResolver, ElementRef, ViewChild } from '@angular/core';
import { Item } from '../../../../../core/shared/item.model';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { getListableObjectComponent, listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import {
getListableObjectComponent,
listableObjectComponent
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { Context } from '../../../../../core/shared/context.model';
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
@@ -13,9 +16,13 @@ import { Observable } from 'rxjs';
import { LinkService } from '../../../../../core/cache/builders/link.service';
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
import { RemoteData } from '../../../../../core/data/remote-data';
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators';
import {
getAllSucceededRemoteData,
getRemoteDataPayload
} from '../../../../../core/shared/operators';
import { take } from 'rxjs/operators';
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
@listableObjectComponent(WorkflowItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch)
@Component({
@@ -51,6 +58,7 @@ export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends S
private componentFactoryResolver: ComponentFactoryResolver,
private linkService: LinkService,
protected truncatableService: TruncatableService,
private themeService: ThemeService,
protected bitstreamDataService: BitstreamDataService
) {
super(truncatableService, bitstreamDataService);
@@ -92,7 +100,7 @@ export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends S
* @returns {GenericConstructor<Component>}
*/
private getComponent(item: Item): GenericConstructor<Component> {
return getListableObjectComponent(item.getRenderTypes(), ViewMode.GridElement, undefined);
return getListableObjectComponent(item.getRenderTypes(), ViewMode.GridElement, undefined, this.themeService.getThemeName());
}
}

View File

@@ -16,6 +16,8 @@ import { Item } from '../../../../../core/shared/item.model';
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
describe('WorkflowItemAdminWorkflowListElementComponent', () => {
let component: WorkflowItemSearchResultAdminWorkflowListElementComponent;
@@ -49,6 +51,7 @@ describe('WorkflowItemAdminWorkflowListElementComponent', () => {
providers: [
{ provide: TruncatableService, useValue: mockTruncatableService },
{ provide: LinkService, useValue: linkService },
{ provide: DSONameService, useClass: DSONameServiceMock }
],
schemas: [NO_ERRORS_SCHEMA]
})

View File

@@ -12,6 +12,7 @@ import { Item } from '../../../../../core/shared/item.model';
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
@listableObjectComponent(WorkflowItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch)
@Component({
@@ -29,8 +30,11 @@ export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends S
*/
public item$: Observable<Item>;
constructor(private linkService: LinkService, protected truncatableService: TruncatableService) {
super(truncatableService);
constructor(private linkService: LinkService,
protected truncatableService: TruncatableService,
protected dsoNameService: DSONameService
) {
super(truncatableService, dsoNameService);
}
/**

View File

@@ -2,7 +2,7 @@
::ng-deep {
.switch {
position: absolute;
top: $spacer*2.5;
top: calc(var(--bs-spacer) * 2.5);
}
}
}

View File

@@ -18,10 +18,14 @@ import { hasValue } from '../../shared/empty.util';
import { FormControl, FormGroup } from '@angular/forms';
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
import { VarDirective } from '../../shared/utils/var.directive';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import {
createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$
} from '../../shared/remote-data.utils';
import { RouterStub } from '../../shared/testing/router.stub';
import { getItemEditRoute } from '../../+item-page/item-page-routing-paths';
import { getEntityEditRoute, getItemEditRoute } from '../../+item-page/item-page-routing-paths';
import { createPaginatedList } from '../../shared/testing/utils.test';
import { Item } from '../../core/shared/item.model';
const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info');
const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');
@@ -109,9 +113,9 @@ describe('EditBitstreamPageComponent', () => {
self: 'bitstream-selflink'
},
bundle: createSuccessfulRemoteDataObject$({
item: createSuccessfulRemoteDataObject$({
item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), {
uuid: 'some-uuid'
})
}))
})
});
bitstreamService = jasmine.createSpyObj('bitstreamService', {
@@ -237,14 +241,14 @@ describe('EditBitstreamPageComponent', () => {
it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => {
comp.itemId = 'some-uuid1';
comp.navigateToItemEditBitstreams();
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute('some-uuid1'), 'bitstreams']);
expect(routerStub.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']);
});
});
describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => {
it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => {
comp.itemId = undefined;
comp.navigateToItemEditBitstreams();
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute('some-uuid'), 'bitstreams']);
expect(routerStub.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']);
});
});
});

View File

@@ -33,9 +33,8 @@ import { Metadata } from '../../core/shared/metadata.utils';
import { Location } from '@angular/common';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { getItemEditRoute } from '../../+item-page/item-page-routing-paths';
import { getEntityEditRoute, getItemEditRoute } from '../../+item-page/item-page-routing-paths';
import { Bundle } from '../../core/shared/bundle.model';
import { Item } from '../../core/shared/item.model';
@Component({
selector: 'ds-edit-bitstream-page',
@@ -264,9 +263,17 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
/**
* The ID of the item the bitstream originates from
* Taken from the current query parameters when present
* This will determine the route of the item edit page to return to
*/
itemId: string;
/**
* The entity type of the item the bitstream originates from
* Taken from the current query parameters when present
* This will determine the route of the item edit page to return to
*/
entityType: string;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
@@ -293,6 +300,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
this.formGroup = this.formService.createFormGroup(this.formModel);
this.itemId = this.route.snapshot.queryParams.itemId;
this.entityType = this.route.snapshot.queryParams.entityType;
this.bitstreamRD$ = this.route.data.pipe(map((data) => data.bitstream));
this.bitstreamFormatsRD$ = this.bitstreamFormatService.findAll(this.findAllOptions);
@@ -499,10 +507,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
*/
navigateToItemEditBitstreams() {
if (hasValue(this.itemId)) {
this.router.navigate([getItemEditRoute(this.itemId), 'bitstreams']);
this.router.navigate([getEntityEditRoute(this.entityType, this.itemId), 'bitstreams']);
} else {
this.bitstream.bundle.pipe(getFirstSucceededRemoteDataPayload(),
mergeMap((bundle: Bundle) => bundle.item.pipe(getFirstSucceededRemoteDataPayload(), map((item: Item) => item.uuid))))
mergeMap((bundle: Bundle) => bundle.item.pipe(getFirstSucceededRemoteDataPayload())))
.subscribe((item) => {
this.router.navigate(([getItemEditRoute(item), 'bitstreams']));
});

View File

@@ -0,0 +1,28 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { BrowseBySwitcherComponent } from './browse-by-switcher.component';
/**
* Themed wrapper for BrowseBySwitcherComponent
*/
@Component({
selector: 'ds-themed-browse-by-switcher',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html'
})
export class ThemedBrowseBySwitcherComponent extends ThemedComponent<BrowseBySwitcherComponent> {
protected getComponentName(): string {
return 'BrowseBySwitcherComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/+browse-by/+browse-by-switcher/browse-by-switcher.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./browse-by-switcher.component`);
}
}

View File

@@ -1,9 +1,9 @@
import { RouterModule } from '@angular/router';
import { NgModule } from '@angular/core';
import { BrowseByGuard } from './browse-by-guard';
import { BrowseBySwitcherComponent } from './+browse-by-switcher/browse-by-switcher.component';
import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver';
import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver';
import { ThemedBrowseBySwitcherComponent } from './+browse-by-switcher/themed-browse-by-switcher.component';
@NgModule({
imports: [
@@ -14,7 +14,7 @@ import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.reso
children: [
{
path: ':id',
component: BrowseBySwitcherComponent,
component: ThemedBrowseBySwitcherComponent,
canActivate: [BrowseByGuard],
resolve: { breadcrumb: BrowseByI18nBreadcrumbResolver },
data: { title: 'browse.title', breadcrumbKey: 'browse.metadata' }

View File

@@ -5,6 +5,7 @@ import { SharedModule } from '../shared/shared.module';
import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component';
import { BrowseByDatePageComponent } from './+browse-by-date-page/browse-by-date-page.component';
import { BrowseBySwitcherComponent } from './+browse-by-switcher/browse-by-switcher.component';
import { ThemedBrowseBySwitcherComponent } from './+browse-by-switcher/themed-browse-by-switcher.component';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
@@ -20,6 +21,7 @@ const ENTRY_COMPONENTS = [
],
declarations: [
BrowseBySwitcherComponent,
ThemedBrowseBySwitcherComponent,
...ENTRY_COMPONENTS
],
exports: [

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { CollectionPageComponent } from './collection-page.component';
import { CollectionPageResolver } from './collection-page.resolver';
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
@@ -21,6 +20,7 @@ import {
import { CollectionPageAdministratorGuard } from './collection-page-administrator.guard';
import { MenuItemType } from '../shared/menu/initial-menus-state';
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
@NgModule({
imports: [
@@ -62,7 +62,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
},
{
path: '',
component: CollectionPageComponent,
component: ThemedCollectionPageComponent,
pathMatch: 'full',
}
],

View File

@@ -35,7 +35,7 @@
</ds-comcol-page-content>
</header>
<div class="pl-2">
<ds-dso-page-edit-button [pageRoutePrefix]="'collections'" [dso]="collection" [tooltipMsg]="'collection.page.edit'"></ds-dso-page-edit-button>
<ds-dso-page-edit-button [pageRoute]="collectionPageRoute$ | async" [dso]="collection" [tooltipMsg]="'collection.page.edit'"></ds-dso-page-edit-button>
</div>
</div>
<section class="comcol-page-browse-section">

View File

@@ -15,13 +15,19 @@ import { Bitstream } from '../core/shared/bitstream.model';
import { Collection } from '../core/shared/collection.model';
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
import { Item } from '../core/shared/item.model';
import { getFirstSucceededRemoteData, redirectOn4xx, toDSpaceObjectListRD } from '../core/shared/operators';
import {
getAllSucceededRemoteDataPayload,
getFirstSucceededRemoteData,
redirectOn4xx,
toDSpaceObjectListRD
} from '../core/shared/operators';
import { fadeIn, fadeInOut } from '../shared/animations/fade';
import { hasValue, isNotEmpty } from '../shared/empty.util';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { AuthService } from '../core/auth/auth.service';
import {PaginationChangeEvent} from '../shared/pagination/paginationChangeEvent.interface';
import { getCollectionPageRoute } from './collection-page-routing-paths';
@Component({
selector: 'ds-collection-page',
@@ -44,6 +50,11 @@ export class CollectionPageComponent implements OnInit {
sortConfig: SortOptions
}>;
/**
* Route to the community page
*/
collectionPageRoute$: Observable<string>;
constructor(
private collectionDataService: CollectionDataService,
private searchService: SearchService,
@@ -94,6 +105,11 @@ export class CollectionPageComponent implements OnInit {
)
);
this.collectionPageRoute$ = this.collectionRD$.pipe(
getAllSucceededRemoteDataPayload(),
map((collection) => getCollectionPageRoute(collection.id))
);
this.route.queryParams.pipe(take(1)).subscribe((params) => {
this.metadata.processRemoteData(this.collectionRD$);
});

View File

@@ -13,6 +13,7 @@ import { CollectionItemMapperComponent } from './collection-item-mapper/collecti
import { SearchService } from '../core/shared/search/search.service';
import { StatisticsModule } from '../statistics/statistics.module';
import { CollectionFormModule } from './collection-form/collection-form.module';
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
@NgModule({
imports: [
@@ -25,6 +26,7 @@ import { CollectionFormModule } from './collection-form/collection-form.module';
],
declarations: [
CollectionPageComponent,
ThemedCollectionPageComponent,
CreateCollectionPageComponent,
DeleteCollectionPageComponent,
EditItemTemplatePageComponent,

View File

@@ -6,17 +6,21 @@ describe('CollectionPageResolver', () => {
describe('resolve', () => {
let resolver: CollectionPageResolver;
let collectionService: any;
let store: any;
const uuid = '1234-65487-12354-1235';
beforeEach(() => {
collectionService = {
findById: (id: string) => createSuccessfulRemoteDataObject$({ id })
};
resolver = new CollectionPageResolver(collectionService);
store = jasmine.createSpyObj('store', {
dispatch: {},
});
resolver = new CollectionPageResolver(collectionService, store);
});
it('should resolve a collection with the correct id', (done) => {
resolver.resolve({ params: { id: uuid } } as any, undefined)
resolver.resolve({ params: { id: uuid } } as any, { url: 'current-url' } as any)
.pipe(first())
.subscribe(
(resolved) => {

View File

@@ -4,15 +4,31 @@ import { Collection } from '../core/shared/collection.model';
import { Observable } from 'rxjs';
import { CollectionDataService } from '../core/data/collection-data.service';
import { RemoteData } from '../core/data/remote-data';
import { followLink } from '../shared/utils/follow-link-config.model';
import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model';
import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { Store } from '@ngrx/store';
import { ResolvedAction } from '../core/resolving/resolver.actions';
/**
* The self links defined in this list are expected to be requested somewhere in the near future
* Requesting them as embeds will limit the number of requests
*/
export const COLLECTION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Collection>[] = [
followLink('parentCommunity', undefined, true, true, true,
followLink('parentCommunity')
),
followLink('logo')
];
/**
* This class represents a resolver that requests a specific collection before the route is activated
*/
@Injectable()
export class CollectionPageResolver implements Resolve<RemoteData<Collection>> {
constructor(private collectionService: CollectionDataService) {
constructor(
private collectionService: CollectionDataService,
private store: Store<any>
) {
}
/**
@@ -23,8 +39,19 @@ export class CollectionPageResolver implements Resolve<RemoteData<Collection>> {
* or an error if something went wrong
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> {
return this.collectionService.findById(route.params.id, true, false, followLink('logo')).pipe(
const collectionRD$ = this.collectionService.findById(
route.params.id,
true,
false,
...COLLECTION_PAGE_LINKS_TO_FOLLOW
).pipe(
getFirstCompletedRemoteData()
);
collectionRD$.subscribe((collectionRD: RemoteData<Collection>) => {
this.store.dispatch(new ResolvedAction(state.url, collectionRD.payload));
});
return collectionRD$;
}
}

View File

@@ -0,0 +1,26 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../shared/theme-support/themed.component';
import { CollectionPageComponent } from './collection-page.component';
/**
* Themed wrapper for CollectionPageComponent
*/
@Component({
selector: 'ds-themed-community-page',
styleUrls: [],
templateUrl: '../shared/theme-support/themed.component.html',
})
export class ThemedCollectionPageComponent extends ThemedComponent<CollectionPageComponent> {
protected getComponentName(): string {
return 'CollectionPageComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../themes/${themeName}/app/+collection-page/collection-page.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./collection-page.component`);
}
}

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { CommunityPageComponent } from './community-page.component';
import { CommunityPageResolver } from './community-page.resolver';
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
@@ -14,6 +13,7 @@ import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-rou
import { CommunityPageAdministratorGuard } from './community-page-administrator.guard';
import { MenuItemType } from '../shared/menu/initial-menus-state';
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { ThemedCommunityPageComponent } from './themed-community-page.component';
@NgModule({
imports: [
@@ -45,7 +45,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
},
{
path: '',
component: CommunityPageComponent,
component: ThemedCommunityPageComponent,
pathMatch: 'full',
}
],

View File

@@ -21,7 +21,7 @@
</ds-comcol-page-content>
</header>
<div class="pl-2">
<ds-dso-page-edit-button [pageRoutePrefix]="'communities'" [dso]="communityPayload" [tooltipMsg]="'community.page.edit'"></ds-dso-page-edit-button>
<ds-dso-page-edit-button [pageRoute]="communityPageRoute$ | async" [dso]="communityPayload" [tooltipMsg]="'community.page.edit'"></ds-dso-page-edit-button>
</div>
</div>
<section class="comcol-page-browse-section">

View File

@@ -13,8 +13,9 @@ import { MetadataService } from '../core/metadata/metadata.service';
import { fadeInOut } from '../shared/animations/fade';
import { hasValue } from '../shared/empty.util';
import { redirectOn4xx } from '../core/shared/operators';
import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../core/shared/operators';
import { AuthService } from '../core/auth/auth.service';
import { getCommunityPageRoute } from './community-page-routing-paths';
@Component({
selector: 'ds-community-page',
@@ -36,6 +37,12 @@ export class CommunityPageComponent implements OnInit {
* The logo of this community
*/
logoRD$: Observable<RemoteData<Bitstream>>;
/**
* Route to the community page
*/
communityPageRoute$: Observable<string>;
constructor(
private communityDataService: CommunityDataService,
private metadata: MetadataService,
@@ -55,6 +62,10 @@ export class CommunityPageComponent implements OnInit {
map((rd: RemoteData<Community>) => rd.payload),
filter((community: Community) => hasValue(community)),
mergeMap((community: Community) => community.logo));
this.communityPageRoute$ = this.communityRD$.pipe(
getAllSucceededRemoteDataPayload(),
map((community) => getCommunityPageRoute(community.id))
);
}
}

View File

@@ -11,6 +11,14 @@ import { CreateCommunityPageComponent } from './create-community-page/create-com
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
import { StatisticsModule } from '../statistics/statistics.module';
import { CommunityFormModule } from './community-form/community-form.module';
import { ThemedCommunityPageComponent } from './themed-community-page.component';
const DECLARATIONS = [CommunityPageComponent,
ThemedCommunityPageComponent,
CommunityPageSubCollectionListComponent,
CommunityPageSubCommunityListComponent,
CreateCommunityPageComponent,
DeleteCommunityPageComponent];
@NgModule({
imports: [
@@ -21,11 +29,10 @@ import { CommunityFormModule } from './community-form/community-form.module';
CommunityFormModule
],
declarations: [
CommunityPageComponent,
CommunityPageSubCollectionListComponent,
CommunityPageSubCommunityListComponent,
CreateCommunityPageComponent,
DeleteCommunityPageComponent
...DECLARATIONS
],
exports: [
...DECLARATIONS
]
})

View File

@@ -6,17 +6,21 @@ describe('CommunityPageResolver', () => {
describe('resolve', () => {
let resolver: CommunityPageResolver;
let communityService: any;
let store: any;
const uuid = '1234-65487-12354-1235';
beforeEach(() => {
communityService = {
findById: (id: string) => createSuccessfulRemoteDataObject$({ id })
};
resolver = new CommunityPageResolver(communityService);
store = jasmine.createSpyObj('store', {
dispatch: {},
});
resolver = new CommunityPageResolver(communityService, store);
});
it('should resolve a community with the correct id', (done) => {
resolver.resolve({ params: { id: uuid } } as any, undefined)
resolver.resolve({ params: { id: uuid } } as any, { url: 'current-url' } as any)
.pipe(first())
.subscribe(
(resolved) => {

View File

@@ -4,15 +4,31 @@ import { Observable } from 'rxjs';
import { RemoteData } from '../core/data/remote-data';
import { Community } from '../core/shared/community.model';
import { CommunityDataService } from '../core/data/community-data.service';
import { followLink } from '../shared/utils/follow-link-config.model';
import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model';
import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { ResolvedAction } from '../core/resolving/resolver.actions';
import { Store } from '@ngrx/store';
/**
* The self links defined in this list are expected to be requested somewhere in the near future
* Requesting them as embeds will limit the number of requests
*/
export const COMMUNITY_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Community>[] = [
followLink('logo'),
followLink('subcommunities'),
followLink('collections'),
followLink('parentCommunity')
];
/**
* This class represents a resolver that requests a specific community before the route is activated
*/
@Injectable()
export class CommunityPageResolver implements Resolve<RemoteData<Community>> {
constructor(private communityService: CommunityDataService) {
constructor(
private communityService: CommunityDataService,
private store: Store<any>
) {
}
/**
@@ -23,15 +39,19 @@ export class CommunityPageResolver implements Resolve<RemoteData<Community>> {
* or an error if something went wrong
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> {
return this.communityService.findById(
const communityRD$ = this.communityService.findById(
route.params.id,
true,
false,
followLink('logo'),
followLink('subcommunities'),
followLink('collections')
...COMMUNITY_PAGE_LINKS_TO_FOLLOW
).pipe(
getFirstCompletedRemoteData(),
);
communityRD$.subscribe((communityRD: RemoteData<Community>) => {
this.store.dispatch(new ResolvedAction(state.url, communityRD.payload));
});
return communityRD$;
}
}

View File

@@ -18,11 +18,14 @@ import { PageInfo } from '../../core/shared/page-info.model';
import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../shared/theme-support/theme.service';
describe('CommunityPageSubCollectionList Component', () => {
let comp: CommunityPageSubCollectionListComponent;
let fixture: ComponentFixture<CommunityPageSubCollectionListComponent>;
let collectionDataServiceStub: any;
let themeService;
let subCollList = [];
const collections = [Object.assign(new Community(), {
@@ -110,6 +113,8 @@ describe('CommunityPageSubCollectionList Component', () => {
}
};
themeService = getMockThemeService();
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
@@ -124,6 +129,7 @@ describe('CommunityPageSubCollectionList Component', () => {
{ provide: CollectionDataService, useValue: collectionDataServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: SelectableListService, useValue: {} },
{ provide: ThemeService, useValue: themeService },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

View File

@@ -18,11 +18,14 @@ import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { CommunityDataService } from '../../core/data/community-data.service';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../shared/theme-support/theme.service';
describe('CommunityPageSubCommunityListComponent Component', () => {
let comp: CommunityPageSubCommunityListComponent;
let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
let communityDataServiceStub: any;
let themeService;
let subCommList = [];
const subcommunities = [Object.assign(new Community(), {
@@ -111,6 +114,8 @@ describe('CommunityPageSubCommunityListComponent Component', () => {
}
};
themeService = getMockThemeService();
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
@@ -125,6 +130,7 @@ describe('CommunityPageSubCommunityListComponent Component', () => {
{ provide: CommunityDataService, useValue: communityDataServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: SelectableListService, useValue: {} },
{ provide: ThemeService, useValue: themeService },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

View File

@@ -0,0 +1,26 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../shared/theme-support/themed.component';
import { CommunityPageComponent } from './community-page.component';
/**
* Themed wrapper for CommunityPageComponent
*/
@Component({
selector: 'ds-themed-community-page',
styleUrls: [],
templateUrl: '../shared/theme-support/themed.component.html',
})
export class ThemedCommunityPageComponent extends ThemedComponent<CommunityPageComponent> {
protected getComponentName(): string {
return 'CommunityPageComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../themes/${themeName}/app/+community-page/community-page.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./community-page.component`);
}
}

View File

@@ -1,7 +1,7 @@
:host {
display: block;
margin-top: -$content-spacing;
margin-bottom: -$content-spacing;
margin-top: calc(-1 * var(--ds-content-spacing));
margin-bottom: calc(-1 * var(--ds-content-spacing));
}
.display-3 {

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { Component, } from '@angular/core';
@Component({
selector: 'ds-home-news',
@@ -10,5 +10,4 @@ import { Component } from '@angular/core';
* Component to render the news section on the home page
*/
export class HomeNewsComponent {
}

View File

@@ -0,0 +1,27 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { HomeNewsComponent } from './home-news.component';
@Component({
selector: 'ds-themed-home-news',
styleUrls: [],
templateUrl: '../../shared/theme-support/themed.component.html',
})
/**
* Component to render the news section on the home page
*/
export class ThemedHomeNewsComponent extends ThemedComponent<HomeNewsComponent> {
protected getComponentName(): string {
return 'HomeNewsComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/+home-page/home-news/home-news.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./home-news.component`);
}
}

View File

@@ -1,17 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { HomePageComponent } from './home-page.component';
import { HomePageResolver } from './home-page.resolver';
import { MenuItemType } from '../shared/menu/initial-menus-state';
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { ThemedHomePageComponent } from './themed-home-page.component';
@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: HomePageComponent,
component: ThemedHomePageComponent,
pathMatch: 'full',
data: {
title: 'home.title',

View File

@@ -1,4 +1,4 @@
<ds-home-news></ds-home-news>
<ds-themed-home-news></ds-themed-home-news>
<div class="container">
<ng-container *ngIf="(site$ | async) as site">
<ds-view-tracker [object]="site"></ds-view-tracker>

View File

@@ -7,6 +7,16 @@ import { HomePageRoutingModule } from './home-page-routing.module';
import { HomePageComponent } from './home-page.component';
import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component';
import { StatisticsModule } from '../statistics/statistics.module';
import { ThemedHomeNewsComponent } from './home-news/themed-home-news.component';
import { ThemedHomePageComponent } from './themed-home-page.component';
const DECLARATIONS = [
HomePageComponent,
ThemedHomePageComponent,
TopLevelCommunityListComponent,
ThemedHomeNewsComponent,
HomeNewsComponent,
];
@NgModule({
imports: [
@@ -16,10 +26,11 @@ import { StatisticsModule } from '../statistics/statistics.module';
StatisticsModule.forRoot()
],
declarations: [
HomePageComponent,
TopLevelCommunityListComponent,
HomeNewsComponent,
]
...DECLARATIONS,
],
exports: [
...DECLARATIONS,
],
})
export class HomePageModule {

View File

@@ -0,0 +1,26 @@
import { ThemedComponent } from '../shared/theme-support/themed.component';
import { HomePageComponent } from './home-page.component';
import { Component } from '@angular/core';
@Component({
selector: 'ds-themed-home-page',
styleUrls: [],
templateUrl: '../shared/theme-support/themed.component.html',
})
export class ThemedHomePageComponent extends ThemedComponent<HomePageComponent> {
protected inAndOutputNames: (keyof HomePageComponent & keyof this)[];
protected getComponentName(): string {
return 'HomePageComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../themes/${themeName}/app/+home-page/home-page.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./home-page.component`);
}
}

View File

@@ -18,11 +18,14 @@ import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { CommunityDataService } from '../../core/data/community-data.service';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../shared/theme-support/theme.service';
describe('TopLevelCommunityList Component', () => {
let comp: TopLevelCommunityListComponent;
let fixture: ComponentFixture<TopLevelCommunityListComponent>;
let communityDataServiceStub: any;
let themeService;
const topCommList = [Object.assign(new Community(), {
id: '123456789-1',
@@ -101,6 +104,8 @@ describe('TopLevelCommunityList Component', () => {
}
};
themeService = getMockThemeService();
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
@@ -115,6 +120,7 @@ describe('TopLevelCommunityList Component', () => {
{ provide: CommunityDataService, useValue: communityDataServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: SelectableListService, useValue: {} },
{ provide: ThemeService, useValue: themeService },
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

View File

@@ -1,7 +1,7 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
import { SubmissionImportExternalComponent } from '../submission/import-external/submission-import-external.component';
import { ThemedSubmissionImportExternalComponent } from '../submission/import-external/themed-submission-import-external.component';
@NgModule({
imports: [
@@ -9,7 +9,7 @@ import { SubmissionImportExternalComponent } from '../submission/import-external
{
canActivate: [ AuthenticatedGuard ],
path: '',
component: SubmissionImportExternalComponent,
component: ThemedSubmissionImportExternalComponent,
pathMatch: 'full',
data: {
title: 'submission.import-external.page.title'

View File

@@ -17,7 +17,7 @@ import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operato
import { UploaderComponent } from '../../../shared/uploader/uploader.component';
import { RequestService } from '../../../core/data/request.service';
import { getBitstreamModuleRoute } from '../../../app-routing-paths';
import { getItemEditRoute } from '../../item-page-routing-paths';
import { getEntityEditRoute } from '../../item-page-routing-paths';
@Component({
selector: 'ds-upload-bitstream',
@@ -37,6 +37,12 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy {
*/
itemId: string;
/**
* The entity type of the item
* This is fetched from the current URL and will determine the item's page route
*/
entityType: string;
/**
* The item to upload a bitstream to
*/
@@ -100,6 +106,7 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy {
*/
ngOnInit(): void {
this.itemId = this.route.snapshot.params.id;
this.entityType = this.route.snapshot.params['entity-type'];
this.itemRD$ = this.route.data.pipe(map((data) => data.dso));
this.bundlesRD$ = this.itemRD$.pipe(
switchMap((itemRD: RemoteData<Item>) => itemRD.payload.bundles)
@@ -167,7 +174,7 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy {
});
// Bring over the item ID as a query parameter
const queryParams = { itemId: this.itemId };
const queryParams = { itemId: this.itemId, entityType: this.entityType };
this.router.navigate([getBitstreamModuleRoute(), bitstream.id, 'edit'], { queryParams: queryParams });
}
@@ -193,7 +200,7 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy {
* When cancel is clicked, navigate back to the item's edit bitstreams page
*/
onCancel() {
this.router.navigate([getItemEditRoute(this.itemId), 'bitstreams']);
this.router.navigate([getEntityEditRoute(this.entityType, this.itemId), 'bitstreams']);
}
/**

View File

@@ -14,6 +14,7 @@ import { first, map, switchMap, tap } from 'rxjs/operators';
import { RemoteData } from '../../../core/data/remote-data';
import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component';
import { environment } from '../../../../environments/environment';
import { getItemPageRoute } from '../../item-page-routing-paths';
import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page.resolver';
import { getAllSucceededRemoteData } from '../../../core/shared/operators';
import { hasValue } from '../../../shared/empty.util';
@@ -36,6 +37,11 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
*/
updates$: Observable<FieldUpdates>;
/**
* Route to the item's page
*/
itemPageRoute: string;
/**
* A subscription that checks when the item is deleted in cache and reloads the item by sending a new request
* This is used to update the item in cache after bitstreams are deleted
@@ -69,6 +75,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
getAllSucceededRemoteData()
).subscribe((rd: RemoteData<Item>) => {
this.item = rd.payload;
this.itemPageRoute = getItemPageRoute(this.item);
this.postItemInit();
this.initializeUpdates();
});

View File

@@ -1,3 +1,3 @@
.btn {
min-width: $edit-item-button-min-width;
min-width: var(--ds-edit-item-button-min-width);
}

View File

@@ -55,6 +55,6 @@ export class EditItemPageComponent implements OnInit {
* @param item The item for which the url is requested
*/
getItemPage(item: Item): string {
return getItemPageRoute(item.id);
return getItemPageRoute(item);
}
}

View File

@@ -74,18 +74,18 @@ import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
component: ItemRelationshipsComponent,
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true }
},
/* TODO - uncomment & fix when view page exists
{
path: 'view',
/* TODO - change when view page exists */
component: ItemBitstreamsComponent,
data: { title: 'item.edit.tabs.view.title', showBreadcrumbs: true }
},
}, */
/* TODO - uncomment & fix when curate page exists
{
path: 'curate',
/* TODO - change when curate page exists */
component: ItemBitstreamsComponent,
data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true }
},
}, */
{
path: 'versionhistory',
component: ItemVersionHistoryComponent,

View File

@@ -1,7 +1,7 @@
<div class="item-bitstreams" *ngVar="(bundles$ | async) as bundles">
<div class="button-row top d-flex mt-2">
<button class="mr-auto btn btn-success"
[routerLink]="['/items/', item.id, 'bitstreams', 'new']"><i
[routerLink]="[itemPageRoute, 'bitstreams', 'new']"><i
class="fas fa-upload"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.upload-button" | translate}}</span>
</button>

View File

@@ -1,19 +1,19 @@
.header-row {
color: $table-dark-color;
background-color: $table-dark-bg;
border-color: $table-dark-border-color;
color: var(--bs-table-dark-color);
background-color: var(--bs-table-dark-bg);
border-color: var(--bs-table-dark-border-color);
}
.bundle-row {
color: $table-head-color;
background-color: $table-head-bg;
border-color: $table-border-color;
color: var(--bs-table-head-color);
background-color: var(--bs-table-head-bg);
border-color: var(--bs-table-border-color);
}
.row-element {
padding: 12px;
padding: 0.75em;
border-bottom: $table-border-width solid $table-border-color;
border-bottom: var(--bs-table-border-width) solid var(--bs-table-border-color);
}
.drag-handle {

View File

@@ -8,7 +8,7 @@
</div>
<div class="{{columnSizes.columns[3].buildClasses()}} text-center row-element">
<div class="btn-group bundle-action-buttons">
<button [routerLink]="['/items/', item.id, 'bitstreams', 'new']"
<button [routerLink]="[itemPageRoute, 'bitstreams', 'new']"
[queryParams]="{bundle: bundle.id}"
class="btn btn-outline-success btn-sm"
title="{{'item.edit.bitstreams.bundle.edit.buttons.upload' | translate}}">

View File

@@ -3,6 +3,7 @@ import { Bundle } from '../../../../core/shared/bundle.model';
import { Item } from '../../../../core/shared/item.model';
import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes';
import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes';
import { getItemPageRoute } from '../../../item-page-routing-paths';
@Component({
selector: 'ds-item-edit-bitstream-bundle',
@@ -49,11 +50,17 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
*/
bundleNameColumn: ResponsiveColumnSizes;
/**
* Route to the item's page
*/
itemPageRoute: string;
constructor(private viewContainerRef: ViewContainerRef) {
}
ngOnInit(): void {
this.bundleNameColumn = this.columnSizes.combineColumns(0, 2);
this.viewContainerRef.createEmbeddedView(this.bundleView);
this.itemPageRoute = getItemPageRoute(this.item);
}
}

View File

@@ -55,6 +55,7 @@ describe('ItemCollectionMapperComponent', () => {
const mockCollection = Object.assign(new Collection(), { id: 'collection1' });
const mockItem: Item = Object.assign(new Item(), {
id: '932c7d50-d85a-44cb-b9dc-b427b12877bd',
uuid: '932c7d50-d85a-44cb-b9dc-b427b12877bd',
name: 'test-item'
});
const mockItemRD: RemoteData<Item> = createSuccessfulRemoteDataObject(mockItem);
@@ -212,7 +213,7 @@ describe('ItemCollectionMapperComponent', () => {
});
it('should navigate to the item page', () => {
expect(router.navigate).toHaveBeenCalledWith(['/items/', mockItem.id]);
expect(router.navigate).toHaveBeenCalledWith(['/items/' + mockItem.uuid]);
});
});

View File

@@ -26,6 +26,7 @@ import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
import { SearchService } from '../../../core/shared/search/search.service';
import { NoContent } from '../../../core/shared/NoContent.model';
import { getItemPageRoute } from '../../item-page-routing-paths';
@Component({
selector: 'ds-item-collection-mapper',
@@ -312,7 +313,7 @@ export class ItemCollectionMapperComponent implements OnInit {
getRemoteDataPayload(),
take(1)
).subscribe((item: Item) => {
this.router.navigate(['/items/', item.id]);
this.router.navigate([getItemPageRoute(item)]);
});
}

View File

@@ -89,7 +89,7 @@
<button (click)="performAction()"
class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}}
</button>
<button [routerLink]="['/items/', item.id, 'edit']" class="btn btn-outline-secondary cancel">
<button [routerLink]="[itemPageRoute, 'edit']" class="btn btn-outline-secondary cancel">
{{cancelMessage| translate}}
</button>

View File

@@ -183,7 +183,7 @@ describe('ItemDeleteComponent', () => {
describe('notify', () => {
it('should navigate to the item edit page on failed deletion of the item', () => {
comp.notify(false);
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute('fake-id')]);
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute(mockItem)]);
});
});
});

View File

@@ -355,7 +355,7 @@ export class ItemDeleteComponent
this.router.navigate(['']);
} else {
this.notificationsService.error(this.translateService.get('item.edit.' + this.messageKey + '.error'));
this.router.navigate([getItemEditRoute(this.item.id)]);
this.router.navigate([getItemEditRoute(this.item)]);
}
}
}

View File

@@ -1,13 +1,13 @@
.btn[disabled] {
color: $gray-600;
border-color: $gray-600;
color: var(--bs-gray-600);
border-color: var(--bs-gray-600);
z-index: 0; // prevent border colors jumping on hover
}
.metadata-field {
width: $edit-item-metadata-field-width;
width: var(--ds-edit-item-metadata-field-width);
}
.language-field {
width: $edit-item-language-field-width;
width: var(--ds-edit-item-language-field-width);
}

View File

@@ -1,19 +1,19 @@
.button-row {
.btn {
margin-right: 0.5 * $spacer;
margin-right: calc(0.5 * var(--bs-spacer));
&:last-child {
margin-right: 0;
}
@media screen and (min-width: map-get($grid-breakpoints, sm)) {
min-width: $edit-item-button-min-width;
min-width: var(--ds-edit-item-button-min-width);
}
}
&.top .btn {
margin-top: $spacer/2;
margin-bottom: $spacer/2;
margin-top: calc(var(--bs-spacer) / 2);
margin-bottom: calc(var(--bs-spacer) / 2);
}

View File

@@ -39,7 +39,7 @@
{{'item.edit.move.processing' | translate}}
</span>
</button>
<button [routerLink]="['/items/', (itemRD$ | async)?.payload?.id, 'edit']"
<button [routerLink]="[(itemPageRoute$ | async), 'edit']"
class="btn btn-outline-secondary">
{{'item.edit.move.cancel' | translate}}
</button>

View File

@@ -57,9 +57,9 @@ describe('ItemMoveComponent', () => {
const routeStub = {
data: observableOf({
dso: createSuccessfulRemoteDataObject({
dso: createSuccessfulRemoteDataObject(Object.assign(new Item(), {
id: 'item1'
})
}))
})
};
@@ -122,7 +122,10 @@ describe('ItemMoveComponent', () => {
});
describe('moveCollection', () => {
it('should call itemDataService.moveToCollection', () => {
comp.itemId = 'item-id';
comp.item = Object.assign(new Item(), {
id: 'item-id',
uuid: 'item-id',
});
comp.selectedCollectionName = 'selected-collection-id';
comp.selectedCollection = collection1;
comp.moveCollection();

View File

@@ -10,7 +10,7 @@ import { NotificationsService } from '../../../shared/notifications/notification
import { TranslateService } from '@ngx-translate/core';
import {
getFirstSucceededRemoteData,
getFirstCompletedRemoteData
getFirstCompletedRemoteData, getAllSucceededRemoteDataPayload
} from '../../../core/shared/operators';
import { ItemDataService } from '../../../core/data/item-data.service';
import { Observable, of as observableOf } from 'rxjs';
@@ -19,7 +19,7 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio
import { SearchService } from '../../../core/shared/search/search.service';
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
import { SearchResult } from '../../../shared/search/search-result.model';
import { getItemEditRoute } from '../../item-page-routing-paths';
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
@Component({
selector: 'ds-item-move',
@@ -43,11 +43,16 @@ export class ItemMoveComponent implements OnInit {
selectedCollection: Collection;
canSubmit = false;
itemId: string;
item: Item;
processing = false;
pagination = new PaginationComponentOptions();
/**
* Route to the item's page
*/
itemPageRoute$: Observable<string>;
constructor(private route: ActivatedRoute,
private router: Router,
private notificationsService: NotificationsService,
@@ -58,8 +63,12 @@ export class ItemMoveComponent implements OnInit {
ngOnInit(): void {
this.itemRD$ = this.route.data.pipe(map((data) => data.dso), getFirstSucceededRemoteData()) as Observable<RemoteData<Item>>;
this.itemPageRoute$ = this.itemRD$.pipe(
getAllSucceededRemoteDataPayload(),
map((item) => getItemPageRoute(item))
);
this.itemRD$.subscribe((rd) => {
this.itemId = rd.payload.id;
this.item = rd.payload;
}
);
this.pagination.pageSize = 5;
@@ -116,9 +125,9 @@ export class ItemMoveComponent implements OnInit {
*/
moveCollection() {
this.processing = true;
this.itemDataService.moveToCollection(this.itemId, this.selectedCollection).pipe(getFirstCompletedRemoteData()).subscribe(
this.itemDataService.moveToCollection(this.item.id, this.selectedCollection).pipe(getFirstCompletedRemoteData()).subscribe(
(response: RemoteData<Collection>) => {
this.router.navigate([getItemEditRoute(this.itemId)]);
this.router.navigate([getItemEditRoute(this.item)]);
if (response.hasSucceeded) {
this.notificationsService.success(this.translateService.get('item.edit.move.success'));
} else {

View File

@@ -47,9 +47,9 @@ describe('ItemReinstateComponent', () => {
routeStub = {
data: observableOf({
dso: createSuccessfulRemoteDataObject({
dso: createSuccessfulRemoteDataObject(Object.assign(new Item(), {
id: 'fake-id'
})
}))
})
};

View File

@@ -1,10 +1,10 @@
.relationship-row:not(.alert) {
padding: $alert-padding-y 0;
padding: var(--bs-alert-padding-y) 0;
}
.relationship-row.alert {
margin-left: -$alert-padding-x;
margin-right: -$alert-padding-x;
margin-left: calc(-1 * var(--bs-alert-padding-x));
margin-right: calc(-1 * var(--bs-alert-padding-x));
margin-top: -1px;
margin-bottom: -1px;
}

View File

@@ -1,6 +1,6 @@
.btn[disabled] {
color: $gray-600;
border-color: $gray-600;
color: var(--bs-gray-600);
border-color: var(--bs-gray-600);
z-index: 0; // prevent border colors jumping on hover
}

View File

@@ -1,19 +1,19 @@
.button-row {
.btn {
margin-right: 0.5 * $spacer;
margin-right: calc(0.5 * var(--bs-spacer));
&:last-child {
margin-right: 0;
}
@media screen and (min-width: map-get($grid-breakpoints, sm)) {
min-width: $edit-item-button-min-width;
min-width: var(--ds-edit-item-button-min-width);
}
}
&.top .btn {
margin-top: $spacer/2;
margin-bottom: $spacer/2;
margin-top: calc(var(--bs-spacer) / 2);
margin-bottom: calc(var(--bs-spacer) / 2);
}

View File

@@ -12,7 +12,7 @@
{{'item.edit.tabs.status.labels.itemPage' | translate}}:
</div>
<div class="col-9 float-left status-data" id="status-itemPage">
<a [routerLink]="getItemPage((itemRD$ | async)?.payload)">{{getItemPage((itemRD$ | async)?.payload)}}</a>
<a [routerLink]="itemPageRoute$ | async">{{itemPageRoute$ | async}}</a>
</div>
<div *ngFor="let operation of (operations$ | async)" class="w-100" [ngClass]="{'pt-3': operation}">

View File

@@ -20,6 +20,7 @@ describe('ItemStatusComponent', () => {
const mockItem = Object.assign(new Item(), {
id: 'fake-id',
uuid: 'fake-id',
handle: 'fake/handle',
lastModified: '2018',
_links: {
@@ -27,7 +28,7 @@ describe('ItemStatusComponent', () => {
}
});
const itemPageUrl = `items/${mockItem.id}`;
const itemPageUrl = `/items/${mockItem.uuid}`;
const routeStub = {
parent: {

View File

@@ -10,6 +10,7 @@ import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-path
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { hasValue } from '../../../shared/empty.util';
import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators';
@Component({
selector: 'ds-item-status',
@@ -50,6 +51,11 @@ export class ItemStatusComponent implements OnInit {
*/
actionsKeys;
/**
* Route to the item's page
*/
itemPageRoute$: Observable<string>;
constructor(private route: ActivatedRoute,
private authorizationService: AuthorizationDataService) {
}
@@ -109,15 +115,10 @@ export class ItemStatusComponent implements OnInit {
});
}
});
}
/**
* Get the url to the simple item page
* @returns {string} url
*/
getItemPage(item: Item): string {
return getItemPageRoute(item.id);
this.itemPageRoute$ = this.itemRD$.pipe(
getAllSucceededRemoteDataPayload(),
map((item) => getItemPageRoute(item))
);
}
/**
@@ -125,7 +126,7 @@ export class ItemStatusComponent implements OnInit {
* @returns {string} url
*/
getCurrentUrl(item: Item): string {
return getItemEditRoute(item.id);
return getItemEditRoute(item);
}
trackOperation(index: number, operation: ItemOperation) {

View File

@@ -6,7 +6,7 @@
<ds-modify-item-overview [item]="item"></ds-modify-item-overview>
<button (click)="performAction()" class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}}
</button>
<button [routerLink]="['/items/', item.id, 'edit']" class="btn btn-outline-secondary cancel">
<button [routerLink]="[itemPageRoute, 'edit']" class="btn btn-outline-secondary cancel">
{{cancelMessage| translate}}
</button>

View File

@@ -74,9 +74,9 @@ describe('AbstractSimpleItemActionComponent', () => {
routeStub = {
data: observableOf({
dso: createSuccessfulRemoteDataObject({
dso: createSuccessfulRemoteDataObject(Object.assign(new Item(), {
id: 'fake-id'
})
}))
})
};
@@ -136,14 +136,14 @@ describe('AbstractSimpleItemActionComponent', () => {
comp.processRestResponse(successfulRemoteData);
expect(notificationsServiceStub.success).toHaveBeenCalled();
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute(mockItem.id)]);
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute(mockItem)]);
});
it('should process a RemoteData to navigate and display success notification', () => {
comp.processRestResponse(failedRemoteData);
expect(notificationsServiceStub.error).toHaveBeenCalled();
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute(mockItem.id)]);
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute(mockItem)]);
});
});

View File

@@ -9,7 +9,7 @@ import { Observable } from 'rxjs';
import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
import { first, map } from 'rxjs/operators';
import { findSuccessfulAccordingTo } from '../edit-item-operators';
import { getItemEditRoute } from '../../item-page-routing-paths';
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
/**
* Component to render and handle simple item edit actions such as withdrawal and reinstatement.
@@ -30,6 +30,11 @@ export class AbstractSimpleItemActionComponent implements OnInit {
headerMessage: string;
descriptionMessage: string;
/**
* Route to the item's page
*/
itemPageRoute: string;
protected predicate: Predicate<RemoteData<Item>>;
constructor(protected route: ActivatedRoute,
@@ -47,6 +52,7 @@ export class AbstractSimpleItemActionComponent implements OnInit {
this.itemRD$.pipe(first()).subscribe((rd) => {
this.item = rd.payload;
this.itemPageRoute = getItemPageRoute(this.item);
}
);
@@ -71,11 +77,11 @@ export class AbstractSimpleItemActionComponent implements OnInit {
this.itemDataService.findById(this.item.id).pipe(
findSuccessfulAccordingTo(this.predicate)).subscribe(() => {
this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success'));
this.router.navigate([getItemEditRoute(this.item.id)]);
this.router.navigate([getItemEditRoute(this.item)]);
});
} else {
this.notificationsService.error(this.translateService.get('item.edit.' + this.messageKey + '.error'));
this.router.navigate([getItemEditRoute(this.item.id)]);
this.router.navigate([getItemEditRoute(this.item)]);
}
}

View File

@@ -7,11 +7,11 @@
<div class="d-flex flex-row">
<ds-item-page-title-field class="mr-auto" [item]="item"></ds-item-page-title-field>
<div class="pl-2">
<ds-dso-page-edit-button [pageRoutePrefix]="'items'" [dso]="item" [tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute$ | async" [dso]="item" [tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button>
</div>
</div>
<div class="simple-view-link my-3">
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id]">
<a class="btn btn-outline-primary" [routerLink]="[(itemPageRoute$ | async)]">
{{"item.page.link.simple" | translate}}
</a>
</div>

View File

@@ -18,9 +18,8 @@ import { hasValue } from '../../shared/empty.util';
import { AuthService } from '../../core/auth/auth.service';
/**
* This component renders a simple item page.
* This component renders a full item page.
* The route parameter 'id' is used to request the item it represents.
* All fields of the item that should be displayed, are defined in its template.
*/
@Component({

View File

@@ -0,0 +1,25 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { FullItemPageComponent } from './full-item-page.component';
/**
* Themed wrapper for FullItemPageComponent
*/
@Component({
selector: 'ds-themed-full-item-page',
styleUrls: [],
templateUrl: './../../shared/theme-support/themed.component.html',
})
export class ThemedFullItemPageComponent extends ThemedComponent<FullItemPageComponent> {
protected getComponentName(): string {
return 'FullItemPageComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/+item-page/full/full-item-page.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./full-item-page.component`);
}
}

View File

@@ -1,4 +1,6 @@
import { URLCombiner } from '../core/url-combiner/url-combiner';
import { Item } from '../core/shared/item.model';
import { isNotEmpty } from '../shared/empty.util';
export const ITEM_MODULE_PATH = 'items';
@@ -6,12 +8,30 @@ export function getItemModuleRoute() {
return `/${ITEM_MODULE_PATH}`;
}
export function getItemPageRoute(itemId: string) {
return new URLCombiner(getItemModuleRoute(), itemId).toString();
/**
* Get the route to an item's page
* Depending on the item's relationship type, the route will either start with /items or /entities
* @param item The item to retrieve the route for
*/
export function getItemPageRoute(item: Item) {
const type = item.firstMetadataValue('relationship.type');
return getEntityPageRoute(type, item.uuid);
}
export function getItemEditRoute(id: string) {
return new URLCombiner(getItemModuleRoute(), id, ITEM_EDIT_PATH).toString();
export function getItemEditRoute(item: Item) {
return new URLCombiner(getItemPageRoute(item), ITEM_EDIT_PATH).toString();
}
export function getEntityPageRoute(entityType: string, itemId: string) {
if (isNotEmpty(entityType)) {
return new URLCombiner('/entities', encodeURIComponent(entityType.toLowerCase()), itemId).toString();
} else {
return new URLCombiner(getItemModuleRoute(), itemId).toString();
}
}
export function getEntityEditRoute(entityType: string, itemId: string) {
return new URLCombiner(getEntityPageRoute(entityType, itemId), ITEM_EDIT_PATH).toString();
}
export const ITEM_EDIT_PATH = 'edit';

View File

@@ -1,18 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { ItemPageComponent } from './simple/item-page.component';
import { FullItemPageComponent } from './full/full-item-page.component';
import { ItemPageResolver } from './item-page.resolver';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
import { ItemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver';
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
import { LinkService } from '../core/cache/builders/link.service';
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
import { UPLOAD_BITSTREAM_PATH, ITEM_EDIT_PATH } from './item-page-routing-paths';
import { ITEM_EDIT_PATH, UPLOAD_BITSTREAM_PATH } from './item-page-routing-paths';
import { ItemPageAdministratorGuard } from './item-page-administrator.guard';
import { MenuItemType } from '../shared/menu/initial-menus-state';
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
@NgModule({
imports: [
@@ -27,12 +26,12 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
children: [
{
path: '',
component: ItemPageComponent,
component: ThemedItemPageComponent,
pathMatch: 'full',
},
{
path: 'full',
component: FullItemPageComponent,
component: ThemedFullItemPageComponent,
},
{
path: ITEM_EDIT_PATH,

View File

@@ -25,6 +25,8 @@ import { AbstractIncrementalListComponent } from './simple/abstract-incremental-
import { UntypedItemComponent } from './simple/item-types/untyped-item/untyped-item.component';
import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module';
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
@@ -32,25 +34,11 @@ const ENTRY_COMPONENTS = [
UntypedItemComponent
];
import { MediaViewerComponent } from './media-viewer/media-viewer.component';
import { MediaViewerVideoComponent } from './media-viewer/media-viewer-video/media-viewer-video.component';
import { MediaViewerImageComponent } from './media-viewer/media-viewer-image/media-viewer-image.component';
import { NgxGalleryModule } from '@kolkov/ngx-gallery';
@NgModule({
imports: [
CommonModule,
SharedModule.withEntryComponents(),
ItemPageRoutingModule,
EditItemPageModule,
StatisticsModule.forRoot(),
JournalEntitiesModule.withEntryComponents(),
ResearchEntitiesModule.withEntryComponents(),
NgxGalleryModule,
],
declarations: [
const DECLARATIONS = [
ItemPageComponent,
ThemedItemPageComponent,
FullItemPageComponent,
ThemedFullItemPageComponent,
MetadataUriValuesComponent,
ItemPageAuthorFieldComponent,
ItemPageDateFieldComponent,
@@ -66,9 +54,24 @@ import { NgxGalleryModule } from '@kolkov/ngx-gallery';
ItemComponent,
UploadBitstreamComponent,
AbstractIncrementalListComponent,
MediaViewerComponent,
MediaViewerVideoComponent,
MediaViewerImageComponent,
];
@NgModule({
imports: [
CommonModule,
SharedModule.withEntryComponents(),
ItemPageRoutingModule,
EditItemPageModule,
StatisticsModule.forRoot(),
JournalEntitiesModule.withEntryComponents(),
ResearchEntitiesModule.withEntryComponents(),
NgxGalleryModule,
],
declarations: [
...DECLARATIONS
],
exports: [
...DECLARATIONS
]
})
export class ItemPageModule {

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { RemoteData } from '../core/data/remote-data';
import { ItemDataService } from '../core/data/item-data.service';
@@ -7,9 +7,21 @@ import { Item } from '../core/shared/item.model';
import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model';
import { FindListOptions } from '../core/data/request.models';
import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { Store } from '@ngrx/store';
import { ResolvedAction } from '../core/resolving/resolver.actions';
import { map } from 'rxjs/operators';
import { hasValue } from '../shared/empty.util';
import { getItemPageRoute } from './item-page-routing-paths';
/**
* The self links defined in this list are expected to be requested somewhere in the near future
* Requesting them as embeds will limit the number of requests
*/
export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Item>[] = [
followLink('owningCollection'),
followLink('owningCollection', undefined, true, true, true,
followLink('parentCommunity', undefined, true, true, true,
followLink('parentCommunity'))
),
followLink('bundles', new FindListOptions(), true, true, true, followLink('bitstreams')),
followLink('relationships'),
followLink('version', undefined, true, true, true, followLink('versionhistory')),
@@ -20,7 +32,11 @@ export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Item>[] = [
*/
@Injectable()
export class ItemPageResolver implements Resolve<RemoteData<Item>> {
constructor(private itemService: ItemDataService) {
constructor(
private itemService: ItemDataService,
private store: Store<any>,
private router: Router
) {
}
/**
@@ -31,12 +47,30 @@ export class ItemPageResolver implements Resolve<RemoteData<Item>> {
* or an error if something went wrong
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
return this.itemService.findById(route.params.id,
const itemRD$ = this.itemService.findById(route.params.id,
true,
false,
...ITEM_PAGE_LINKS_TO_FOLLOW
).pipe(
getFirstCompletedRemoteData(),
map((rd: RemoteData<Item>) => {
if (rd.hasSucceeded && hasValue(rd.payload)) {
const itemRoute = getItemPageRoute(rd.payload);
const thisRoute = state.url;
if (!thisRoute.startsWith(itemRoute)) {
const itemId = rd.payload.uuid;
const subRoute = thisRoute.substring(thisRoute.indexOf(itemId) + itemId.length, thisRoute.length);
this.router.navigateByUrl(itemRoute + subRoute);
}
}
return rd;
})
);
itemRD$.subscribe((itemRD: RemoteData<Item>) => {
this.store.dispatch(new ResolvedAction(state.url, itemRD.payload));
});
return itemRD$;
}
}

View File

@@ -11,9 +11,10 @@ import { Item } from '../../core/shared/item.model';
import { MetadataService } from '../../core/metadata/metadata.service';
import { fadeInOut } from '../../shared/animations/fade';
import { redirectOn4xx } from '../../core/shared/operators';
import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../../core/shared/operators';
import { ViewMode } from '../../core/shared/view-mode.model';
import { AuthService } from '../../core/auth/auth.service';
import { getItemPageRoute } from '../item-page-routing-paths';
/**
* This component renders a simple item page.
@@ -44,6 +45,11 @@ export class ItemPageComponent implements OnInit {
*/
viewMode = ViewMode.StandalonePage;
/**
* Route to the item's page
*/
itemPageRoute$: Observable<string>;
constructor(
private route: ActivatedRoute,
private router: Router,
@@ -61,5 +67,9 @@ export class ItemPageComponent implements OnInit {
redirectOn4xx(this.router, this.authService)
);
this.metadataService.processRemoteData(this.itemRD$);
this.itemPageRoute$ = this.itemRD$.pipe(
getAllSucceededRemoteDataPayload(),
map((item) => getItemPageRoute(item))
);
}
}

View File

@@ -3,7 +3,7 @@
{{'publication.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
</h2>
<div class="pl-2">
<ds-dso-page-edit-button [pageRoutePrefix]="'items'" [dso]="object" [tooltipMsg]="'publication.page.edit'"></ds-dso-page-edit-button>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'publication.page.edit'"></ds-dso-page-edit-button>
</div>
</div>
<div class="row">
@@ -79,7 +79,7 @@
</ds-item-page-uri-field>
<ds-item-page-collections [item]="object"></ds-item-page-collections>
<div>
<a class="btn btn-outline-primary" [routerLink]="['/items/' + object.id + '/full']">
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']">
{{"item.page.link.full" | translate}}
</a>
</div>

View File

@@ -1,10 +1,11 @@
import { Component, Input } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../../../../../environments/environment';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
import { Bitstream } from '../../../../core/shared/bitstream.model';
import { Item } from '../../../../core/shared/item.model';
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
import { getItemPageRoute } from '../../../item-page-routing-paths';
@Component({
selector: 'ds-item',
@@ -13,14 +14,22 @@ import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/oper
/**
* A generic component for displaying metadata and relations of an item
*/
export class ItemComponent {
export class ItemComponent implements OnInit {
@Input() object: Item;
/**
* Route to the item page
*/
itemPageRoute: string;
mediaViewer = environment.mediaViewer;
constructor(protected bitstreamDataService: BitstreamDataService) {
}
ngOnInit(): void {
this.itemPageRoute = getItemPageRoute(this.object);
}
// TODO refactor to return RemoteData, and thumbnail template to deal with loading
getThumbnail(): Observable<Bitstream> {
return this.bitstreamDataService.getThumbnailFor(this.object).pipe(

View File

@@ -3,7 +3,7 @@
<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
</h2>
<div class="pl-2">
<ds-dso-page-edit-button [pageRoutePrefix]="'items'" [dso]="object" [tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute" [dso]="object" [tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button>
</div>
</div>
<div class="row">
@@ -64,7 +64,7 @@
</ds-item-page-uri-field>
<ds-item-page-collections [item]="object"></ds-item-page-collections>
<div>
<a class="btn btn-outline-primary" [routerLink]="['/items/' + object.id + '/full']">
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']">
{{"item.page.link.full" | translate}}
</a>
</div>

View File

@@ -0,0 +1,27 @@
import { Component } from '@angular/core';
import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { ItemPageComponent } from './item-page.component';
/**
* Themed wrapper for ItemPageComponent
*/
@Component({
selector: 'ds-themed-item-page',
styleUrls: [],
templateUrl: './../../shared/theme-support/themed.component.html',
})
export class ThemedItemPageComponent extends ThemedComponent<ItemPageComponent> {
protected getComponentName(): string {
return 'ItemPageComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/+item-page/simple/item-page.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./item-page.component`);
}
}

View File

@@ -1,14 +1,13 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { LoginPageComponent } from './login-page.component';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
import { ThemedLoginPageComponent } from './themed-login-page.component';
@NgModule({
imports: [
RouterModule.forChild([
{ path: '', pathMatch: 'full', component: LoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' } }
{ path: '', pathMatch: 'full', component: ThemedLoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' } }
])
],
providers: [

View File

@@ -1,4 +1,4 @@
.login-logo {
height: $login-logo-height;
width: $login-logo-width;
height: var(--ds-login-logo-height);
width: var(--ds-login-logo-width);
}

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