Merge remote-tracking branch 'dspace/main' into w2p-77205_issue-927-Non-site-admin-edit-authorization-group

Conflicts:
	src/app/+collection-page/collection-page.component.html
	src/app/+collection-page/collection-page.component.ts
	src/app/+community-page/community-page.component.html
	src/app/+community-page/community-page.component.ts
	src/app/access-control/group-registry/group-form/members-list/members-list.component.ts
This commit is contained in:
Raf Ponsaerts
2021-03-18 10:37:06 +01:00
417 changed files with 50672 additions and 12974 deletions

View File

@@ -19,7 +19,7 @@ jobs:
strategy: strategy:
# Create a matrix of Node versions to test against (in parallel) # Create a matrix of Node versions to test against (in parallel)
matrix: matrix:
node-version: [10.x, 12.x] node-version: [12.x, 14.x]
# Do NOT exit immediately if one matrix job fails # Do NOT exit immediately if one matrix job fails
fail-fast: false fail-fast: false
# These are the actual CI steps to perform per job # These are the actual CI steps to perform per job

View File

@@ -13,7 +13,7 @@ You can find additional information on the DSpace 7 Angular UI on the [wiki](htt
Quick start 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 ```bash
# clone the repo # clone the repo
@@ -65,7 +65,7 @@ Requirements
------------ ------------
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com) - [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. 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 ├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration
├── typedoc.json * TYPEDOC configuration ├── typedoc.json * TYPEDOC configuration
├── webpack * Webpack (https://webpack.github.io/) config directory ├── webpack * Webpack (https://webpack.github.io/) config directory
│   ├── helpers.js *
│   ├── webpack.aot.js * Webpack (https://webpack.github.io/) config for AoT build │   ├── 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.client.js * Webpack (https://webpack.github.io/) config for client build
│   ├── webpack.common.js * │   ├── webpack.common.js *

View File

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

View File

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

View File

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

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

@@ -16,6 +16,8 @@ import { RouterTestingModule } from '@angular/router/testing';
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-result-grid-element.component'; import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-result-grid-element.component';
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; 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', () => { describe('ItemAdminSearchResultGridElementComponent', () => {
let component: ItemAdminSearchResultGridElementComponent; let component: ItemAdminSearchResultGridElementComponent;
@@ -29,6 +31,8 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
} }
}; };
const mockThemeService = getMockThemeService();
function init() { function init() {
id = '780b2588-bda5-4112-a1cd-0b15000a5339'; id = '780b2588-bda5-4112-a1cd-0b15000a5339';
searchResult = new ItemSearchResult(); searchResult = new ItemSearchResult();
@@ -50,6 +54,7 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
providers: [ providers: [
{ provide: TruncatableService, useValue: mockTruncatableService }, { provide: TruncatableService, useValue: mockTruncatableService },
{ provide: BitstreamDataService, useValue: mockBitstreamDataService }, { provide: BitstreamDataService, useValue: mockBitstreamDataService },
{ provide: ThemeService, useValue: mockThemeService },
], ],
schemas: [NO_ERRORS_SCHEMA] 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 { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
import { GenericConstructor } from '../../../../../core/shared/generic-constructor'; import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; 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) @listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminSearch)
@Component({ @Component({
@@ -29,6 +30,7 @@ export class ItemAdminSearchResultGridElementComponent extends SearchResultGridE
constructor(protected truncatableService: TruncatableService, constructor(protected truncatableService: TruncatableService,
protected bitstreamDataService: BitstreamDataService, protected bitstreamDataService: BitstreamDataService,
private themeService: ThemeService,
private componentFactoryResolver: ComponentFactoryResolver private componentFactoryResolver: ComponentFactoryResolver
) { ) {
super(truncatableService, bitstreamDataService); super(truncatableService, bitstreamDataService);
@@ -63,6 +65,6 @@ export class ItemAdminSearchResultGridElementComponent extends SearchResultGridE
* @returns {GenericConstructor<Component>} * @returns {GenericConstructor<Component>}
*/ */
private getComponent(): 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

@@ -56,19 +56,19 @@ describe('ItemAdminSearchResultActionsComponent', () => {
it('should render an edit button with the correct link', () => { it('should render an edit button with the correct link', () => {
const button = fixture.debugElement.query(By.css('a.edit-link')); const button = fixture.debugElement.query(By.css('a.edit-link'));
const link = button.nativeElement.href; 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', () => { it('should render a delete button with the correct link', () => {
const button = fixture.debugElement.query(By.css('a.delete-link')); const button = fixture.debugElement.query(By.css('a.delete-link'));
const link = button.nativeElement.href; 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', () => { it('should render a move button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.move-link')); const a = fixture.debugElement.query(By.css('a.move-link'));
const link = a.nativeElement.href; 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', () => { describe('when the item is not withdrawn', () => {
@@ -80,7 +80,7 @@ describe('ItemAdminSearchResultActionsComponent', () => {
it('should render a withdraw button with the correct link', () => { it('should render a withdraw button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.withdraw-link')); const a = fixture.debugElement.query(By.css('a.withdraw-link'));
const link = a.nativeElement.href; 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', () => { 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', () => { it('should render a reinstate button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.reinstate-link')); const a = fixture.debugElement.query(By.css('a.reinstate-link'));
const link = a.nativeElement.href; 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', () => { it('should render a make private button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.private-link')); const a = fixture.debugElement.query(By.css('a.private-link'));
const link = a.nativeElement.href; 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', () => { 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', () => { it('should render a make private button with the correct link', () => {
const a = fixture.debugElement.query(By.css('a.public-link')); const a = fixture.debugElement.query(By.css('a.public-link'));
const link = a.nativeElement.href; 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 * Returns the path to the edit page of this item
*/ */
getEditRoute(): string { getEditRoute(): string {
return getItemEditRoute(this.item.uuid); return getItemEditRoute(this.item);
} }
/** /**

View File

@@ -1,12 +1,12 @@
$icon-z-index: 10;
:host { :host {
--ds-icon-z-index: 10;
left: 0; left: 0;
top: 0; top: 0;
height: 100vh; height: 100vh;
flex: 1 1 auto; flex: 1 1 auto;
nav { nav {
background-color: $admin-sidebar-bg; background-color: var(--ds-admin-sidebar-bg);
height: 100%; height: 100%;
flex-direction: column; flex-direction: column;
> div { > div {
@@ -19,12 +19,12 @@ $icon-z-index: 10;
} }
&.inactive ::ng-deep .sidebar-collapsible { &.inactive ::ng-deep .sidebar-collapsible {
margin-left: -#{$sidebar-items-width}; margin-left: calc(-1 * var(--ds-sidebar-items-width));
} }
.navbar-nav { .navbar-nav {
.admin-menu-header { .admin-menu-header {
background-color: $admin-sidebar-header-bg; background-color: var(--ds-admin-sidebar-header-bg);
.logo-wrapper { .logo-wrapper {
img { img {
height: 20px; height: 20px;
@@ -43,29 +43,29 @@ $icon-z-index: 10;
.sidebar-section { .sidebar-section {
display: flex; display: flex;
align-content: stretch; align-content: stretch;
background-color: $admin-sidebar-bg; background-color: var(--ds-admin-sidebar-bg);
.nav-item { .nav-item {
padding-top: $spacer; padding-top: var(--bs-spacer);
padding-bottom: $spacer; padding-bottom: var(--bs-spacer);
} }
.shortcut-icon { .shortcut-icon {
padding-left: $icon-padding; padding-left: var(--ds-icon-padding);
padding-right: $icon-padding; padding-right: var(--ds-icon-padding);
} }
.shortcut-icon, .icon-wrapper { .shortcut-icon, .icon-wrapper {
background-color: inherit; background-color: inherit;
z-index: $icon-z-index; z-index: var(--ds-icon-z-index);
} }
.sidebar-collapsible { .sidebar-collapsible {
width: $sidebar-items-width; width: var(--ds-sidebar-items-width);
position: relative; position: relative;
a { a {
padding-right: $spacer; padding-right: var(--bs-spacer);
width: 100%; width: 100%;
} }
} }
&.active > .sidebar-collapsible > .nav-link { &.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 { :host ::ng-deep {
.fa-chevron-right { .fa-chevron-right {
padding-left: $spacer/2; padding-left: calc(var(--bs-spacer) / 2);
font-size: 0.5rem; font-size: 0.5rem;
line-height: 3; line-height: 3;
} }
.sidebar-sub-level-items { .sidebar-sub-level-items {
list-style: disc; list-style: disc;
color: $navbar-dark-color; color: var(--bs-navbar-dark-color);
overflow: hidden; overflow: hidden;
} }

View File

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

View File

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

@@ -2,7 +2,7 @@
::ng-deep { ::ng-deep {
.switch { .switch {
position: absolute; 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 { FormControl, FormGroup } from '@angular/forms';
import { FileSizePipe } from '../../shared/utils/file-size-pipe'; import { FileSizePipe } from '../../shared/utils/file-size-pipe';
import { VarDirective } from '../../shared/utils/var.directive'; 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 { 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 { createPaginatedList } from '../../shared/testing/utils.test';
import { Item } from '../../core/shared/item.model';
const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info');
const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');
@@ -109,9 +113,9 @@ describe('EditBitstreamPageComponent', () => {
self: 'bitstream-selflink' self: 'bitstream-selflink'
}, },
bundle: createSuccessfulRemoteDataObject$({ bundle: createSuccessfulRemoteDataObject$({
item: createSuccessfulRemoteDataObject$({ item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), {
uuid: 'some-uuid' uuid: 'some-uuid'
}) }))
}) })
}); });
bitstreamService = jasmine.createSpyObj('bitstreamService', { 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', () => { it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => {
comp.itemId = 'some-uuid1'; comp.itemId = 'some-uuid1';
comp.navigateToItemEditBitstreams(); 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', () => { 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 ', () => { it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => {
comp.itemId = undefined; comp.itemId = undefined;
comp.navigateToItemEditBitstreams(); 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 { Location } from '@angular/common';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list.model'; 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 { Bundle } from '../../core/shared/bundle.model';
import { Item } from '../../core/shared/item.model';
@Component({ @Component({
selector: 'ds-edit-bitstream-page', selector: 'ds-edit-bitstream-page',
@@ -264,9 +263,17 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
/** /**
* The ID of the item the bitstream originates from * The ID of the item the bitstream originates from
* Taken from the current query parameters when present * Taken from the current query parameters when present
* This will determine the route of the item edit page to return to
*/ */
itemId: string; 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 * Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array} * @type {Array}
@@ -293,6 +300,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
this.formGroup = this.formService.createFormGroup(this.formModel); this.formGroup = this.formService.createFormGroup(this.formModel);
this.itemId = this.route.snapshot.queryParams.itemId; 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.bitstreamRD$ = this.route.data.pipe(map((data) => data.bitstream));
this.bitstreamFormatsRD$ = this.bitstreamFormatService.findAll(this.findAllOptions); this.bitstreamFormatsRD$ = this.bitstreamFormatService.findAll(this.findAllOptions);
@@ -499,10 +507,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
*/ */
navigateToItemEditBitstreams() { navigateToItemEditBitstreams() {
if (hasValue(this.itemId)) { if (hasValue(this.itemId)) {
this.router.navigate([getItemEditRoute(this.itemId), 'bitstreams']); this.router.navigate([getEntityEditRoute(this.entityType, this.itemId), 'bitstreams']);
} else { } else {
this.bitstream.bundle.pipe(getFirstSucceededRemoteDataPayload(), 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) => { .subscribe((item) => {
this.router.navigate(([getItemEditRoute(item), 'bitstreams'])); this.router.navigate(([getItemEditRoute(item), 'bitstreams']));
}); });

View File

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

View File

@@ -15,7 +15,12 @@ import { Bitstream } from '../core/shared/bitstream.model';
import { Collection } from '../core/shared/collection.model'; import { Collection } from '../core/shared/collection.model';
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model'; import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
import { Item } from '../core/shared/item.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 { fadeIn, fadeInOut } from '../shared/animations/fade';
import { hasValue, isNotEmpty } from '../shared/empty.util'; import { hasValue, isNotEmpty } from '../shared/empty.util';
@@ -24,6 +29,7 @@ import { AuthService } from '../core/auth/auth.service';
import {PaginationChangeEvent} from '../shared/pagination/paginationChangeEvent.interface'; import {PaginationChangeEvent} from '../shared/pagination/paginationChangeEvent.interface';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { FeatureID } from '../core/data/feature-authorization/feature-id';
import { getCollectionPageRoute } from './collection-page-routing-paths';
@Component({ @Component({
selector: 'ds-collection-page', selector: 'ds-collection-page',
@@ -51,6 +57,11 @@ export class CollectionPageComponent implements OnInit {
*/ */
isCollectionAdmin$: Observable<boolean>; isCollectionAdmin$: Observable<boolean>;
/**
* Route to the community page
*/
collectionPageRoute$: Observable<string>;
constructor( constructor(
private collectionDataService: CollectionDataService, private collectionDataService: CollectionDataService,
private searchService: SearchService, private searchService: SearchService,
@@ -103,6 +114,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.route.queryParams.pipe(take(1)).subscribe((params) => {
this.metadata.processRemoteData(this.collectionRD$); this.metadata.processRemoteData(this.collectionRD$);
}); });

View File

@@ -6,17 +6,21 @@ describe('CollectionPageResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: CollectionPageResolver; let resolver: CollectionPageResolver;
let collectionService: any; let collectionService: any;
let store: any;
const uuid = '1234-65487-12354-1235'; const uuid = '1234-65487-12354-1235';
beforeEach(() => { beforeEach(() => {
collectionService = { collectionService = {
findById: (id: string) => createSuccessfulRemoteDataObject$({ id }) 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) => { 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()) .pipe(first())
.subscribe( .subscribe(
(resolved) => { (resolved) => {

View File

@@ -4,15 +4,31 @@ import { Collection } from '../core/shared/collection.model';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { CollectionDataService } from '../core/data/collection-data.service'; import { CollectionDataService } from '../core/data/collection-data.service';
import { RemoteData } from '../core/data/remote-data'; 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 { 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 * This class represents a resolver that requests a specific collection before the route is activated
*/ */
@Injectable() @Injectable()
export class CollectionPageResolver implements Resolve<RemoteData<Collection>> { 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 * or an error if something went wrong
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> { 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() getFirstCompletedRemoteData()
); );
collectionRD$.subscribe((collectionRD: RemoteData<Collection>) => {
this.store.dispatch(new ResolvedAction(state.url, collectionRD.payload));
});
return collectionRD$;
} }
} }

View File

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

View File

@@ -13,10 +13,11 @@ import { MetadataService } from '../core/metadata/metadata.service';
import { fadeInOut } from '../shared/animations/fade'; import { fadeInOut } from '../shared/animations/fade';
import { hasValue } from '../shared/empty.util'; 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 { AuthService } from '../core/auth/auth.service';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { FeatureID } from '../core/data/feature-authorization/feature-id';
import { getCommunityPageRoute } from './community-page-routing-paths';
@Component({ @Component({
selector: 'ds-community-page', selector: 'ds-community-page',
@@ -43,6 +44,12 @@ export class CommunityPageComponent implements OnInit {
* The logo of this community * The logo of this community
*/ */
logoRD$: Observable<RemoteData<Bitstream>>; logoRD$: Observable<RemoteData<Bitstream>>;
/**
* Route to the community page
*/
communityPageRoute$: Observable<string>;
constructor( constructor(
private communityDataService: CommunityDataService, private communityDataService: CommunityDataService,
private metadata: MetadataService, private metadata: MetadataService,
@@ -62,7 +69,10 @@ export class CommunityPageComponent implements OnInit {
this.logoRD$ = this.communityRD$.pipe( this.logoRD$ = this.communityRD$.pipe(
map((rd: RemoteData<Community>) => rd.payload), map((rd: RemoteData<Community>) => rd.payload),
filter((community: Community) => hasValue(community)), filter((community: Community) => hasValue(community)),
mergeMap((community: Community) => community.logo) mergeMap((community: Community) => community.logo));
this.communityPageRoute$ = this.communityRD$.pipe(
getAllSucceededRemoteDataPayload(),
map((community) => getCommunityPageRoute(community.id))
); );
this.isCommunityAdmin$ = this.authorizationDataService.isAuthorized(FeatureID.IsCommunityAdmin); this.isCommunityAdmin$ = this.authorizationDataService.isAuthorized(FeatureID.IsCommunityAdmin);
} }

View File

@@ -6,17 +6,21 @@ describe('CommunityPageResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: CommunityPageResolver; let resolver: CommunityPageResolver;
let communityService: any; let communityService: any;
let store: any;
const uuid = '1234-65487-12354-1235'; const uuid = '1234-65487-12354-1235';
beforeEach(() => { beforeEach(() => {
communityService = { communityService = {
findById: (id: string) => createSuccessfulRemoteDataObject$({ id }) 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) => { 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()) .pipe(first())
.subscribe( .subscribe(
(resolved) => { (resolved) => {

View File

@@ -4,15 +4,31 @@ import { Observable } from 'rxjs';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { Community } from '../core/shared/community.model'; import { Community } from '../core/shared/community.model';
import { CommunityDataService } from '../core/data/community-data.service'; 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 { 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 * This class represents a resolver that requests a specific community before the route is activated
*/ */
@Injectable() @Injectable()
export class CommunityPageResolver implements Resolve<RemoteData<Community>> { 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 * or an error if something went wrong
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> {
return this.communityService.findById( const communityRD$ = this.communityService.findById(
route.params.id, route.params.id,
true, true,
false, false,
followLink('logo'), ...COMMUNITY_PAGE_LINKS_TO_FOLLOW
followLink('subcommunities'),
followLink('collections')
).pipe( ).pipe(
getFirstCompletedRemoteData(), 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 { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.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('CommunityPageSubCollectionList Component', () => { describe('CommunityPageSubCollectionList Component', () => {
let comp: CommunityPageSubCollectionListComponent; let comp: CommunityPageSubCollectionListComponent;
let fixture: ComponentFixture<CommunityPageSubCollectionListComponent>; let fixture: ComponentFixture<CommunityPageSubCollectionListComponent>;
let collectionDataServiceStub: any; let collectionDataServiceStub: any;
let themeService;
let subCollList = []; let subCollList = [];
const collections = [Object.assign(new Community(), { const collections = [Object.assign(new Community(), {
@@ -110,6 +113,8 @@ describe('CommunityPageSubCollectionList Component', () => {
} }
}; };
themeService = getMockThemeService();
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
@@ -124,6 +129,7 @@ describe('CommunityPageSubCollectionList Component', () => {
{ provide: CollectionDataService, useValue: collectionDataServiceStub }, { provide: CollectionDataService, useValue: collectionDataServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: SelectableListService, useValue: {} }, { provide: SelectableListService, useValue: {} },
{ provide: ThemeService, useValue: themeService },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, } from '@angular/core';
@Component({ @Component({
selector: 'ds-home-news', selector: 'ds-home-news',
@@ -10,5 +10,4 @@ import { Component } from '@angular/core';
* Component to render the news section on the home page * Component to render the news section on the home page
*/ */
export class HomeNewsComponent { 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 { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { HomePageComponent } from './home-page.component';
import { HomePageResolver } from './home-page.resolver'; import { HomePageResolver } from './home-page.resolver';
import { MenuItemType } from '../shared/menu/initial-menus-state'; import { MenuItemType } from '../shared/menu/initial-menus-state';
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { ThemedHomePageComponent } from './themed-home-page.component';
@NgModule({ @NgModule({
imports: [ imports: [
RouterModule.forChild([ RouterModule.forChild([
{ {
path: '', path: '',
component: HomePageComponent, component: ThemedHomePageComponent,
pathMatch: 'full', pathMatch: 'full',
data: { data: {
title: 'home.title', 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"> <div class="container">
<ng-container *ngIf="(site$ | async) as site"> <ng-container *ngIf="(site$ | async) as site">
<ds-view-tracker [object]="site"></ds-view-tracker> <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 { HomePageComponent } from './home-page.component';
import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component'; import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component';
import { StatisticsModule } from '../statistics/statistics.module'; 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({ @NgModule({
imports: [ imports: [
@@ -16,10 +26,11 @@ import { StatisticsModule } from '../statistics/statistics.module';
StatisticsModule.forRoot() StatisticsModule.forRoot()
], ],
declarations: [ declarations: [
HomePageComponent, ...DECLARATIONS,
TopLevelCommunityListComponent, ],
HomeNewsComponent, exports: [
] ...DECLARATIONS,
],
}) })
export class HomePageModule { 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 { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.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', () => { describe('TopLevelCommunityList Component', () => {
let comp: TopLevelCommunityListComponent; let comp: TopLevelCommunityListComponent;
let fixture: ComponentFixture<TopLevelCommunityListComponent>; let fixture: ComponentFixture<TopLevelCommunityListComponent>;
let communityDataServiceStub: any; let communityDataServiceStub: any;
let themeService;
const topCommList = [Object.assign(new Community(), { const topCommList = [Object.assign(new Community(), {
id: '123456789-1', id: '123456789-1',
@@ -101,6 +104,8 @@ describe('TopLevelCommunityList Component', () => {
} }
}; };
themeService = getMockThemeService();
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
@@ -115,6 +120,7 @@ describe('TopLevelCommunityList Component', () => {
{ provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: CommunityDataService, useValue: communityDataServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: SelectableListService, useValue: {} }, { provide: SelectableListService, useValue: {} },
{ provide: ThemeService, useValue: themeService },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -17,7 +17,7 @@ import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operato
import { UploaderComponent } from '../../../shared/uploader/uploader.component'; import { UploaderComponent } from '../../../shared/uploader/uploader.component';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../../core/data/request.service';
import { getBitstreamModuleRoute } from '../../../app-routing-paths'; import { getBitstreamModuleRoute } from '../../../app-routing-paths';
import { getItemEditRoute } from '../../item-page-routing-paths'; import { getEntityEditRoute } from '../../item-page-routing-paths';
@Component({ @Component({
selector: 'ds-upload-bitstream', selector: 'ds-upload-bitstream',
@@ -37,6 +37,12 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy {
*/ */
itemId: string; 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 * The item to upload a bitstream to
*/ */
@@ -100,6 +106,7 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy {
*/ */
ngOnInit(): void { ngOnInit(): void {
this.itemId = this.route.snapshot.params.id; 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.itemRD$ = this.route.data.pipe(map((data) => data.dso));
this.bundlesRD$ = this.itemRD$.pipe( this.bundlesRD$ = this.itemRD$.pipe(
switchMap((itemRD: RemoteData<Item>) => itemRD.payload.bundles) 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 // 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 }); 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 * When cancel is clicked, navigate back to the item's edit bitstreams page
*/ */
onCancel() { 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 { RemoteData } from '../../../core/data/remote-data';
import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component'; import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { getItemPageRoute } from '../../item-page-routing-paths';
import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page.resolver'; import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page.resolver';
import { getAllSucceededRemoteData } from '../../../core/shared/operators'; import { getAllSucceededRemoteData } from '../../../core/shared/operators';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
@@ -36,6 +37,11 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
*/ */
updates$: Observable<FieldUpdates>; 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 * 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 * This is used to update the item in cache after bitstreams are deleted
@@ -69,6 +75,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
getAllSucceededRemoteData() getAllSucceededRemoteData()
).subscribe((rd: RemoteData<Item>) => { ).subscribe((rd: RemoteData<Item>) => {
this.item = rd.payload; this.item = rd.payload;
this.itemPageRoute = getItemPageRoute(this.item);
this.postItemInit(); this.postItemInit();
this.initializeUpdates(); this.initializeUpdates();
}); });

View File

@@ -1,3 +1,3 @@
.btn { .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 * @param item The item for which the url is requested
*/ */
getItemPage(item: Item): string { 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, component: ItemRelationshipsComponent,
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true } data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true }
}, },
/* TODO - uncomment & fix when view page exists
{ {
path: 'view', path: 'view',
/* TODO - change when view page exists */
component: ItemBitstreamsComponent, component: ItemBitstreamsComponent,
data: { title: 'item.edit.tabs.view.title', showBreadcrumbs: true } data: { title: 'item.edit.tabs.view.title', showBreadcrumbs: true }
}, }, */
/* TODO - uncomment & fix when curate page exists
{ {
path: 'curate', path: 'curate',
/* TODO - change when curate page exists */
component: ItemBitstreamsComponent, component: ItemBitstreamsComponent,
data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true } data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true }
}, }, */
{ {
path: 'versionhistory', path: 'versionhistory',
component: ItemVersionHistoryComponent, component: ItemVersionHistoryComponent,

View File

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

View File

@@ -1,19 +1,19 @@
.header-row { .header-row {
color: $table-dark-color; color: var(--bs-table-dark-color);
background-color: $table-dark-bg; background-color: var(--bs-table-dark-bg);
border-color: $table-dark-border-color; border-color: var(--bs-table-dark-border-color);
} }
.bundle-row { .bundle-row {
color: $table-head-color; color: var(--bs-table-head-color);
background-color: $table-head-bg; background-color: var(--bs-table-head-bg);
border-color: $table-border-color; border-color: var(--bs-table-border-color);
} }
.row-element { .row-element {
padding: 12px; padding: 12px;
padding: 0.75em; 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 { .drag-handle {

View File

@@ -8,7 +8,7 @@
</div> </div>
<div class="{{columnSizes.columns[3].buildClasses()}} text-center row-element"> <div class="{{columnSizes.columns[3].buildClasses()}} text-center row-element">
<div class="btn-group bundle-action-buttons"> <div class="btn-group bundle-action-buttons">
<button [routerLink]="['/items/', item.id, 'bitstreams', 'new']" <button [routerLink]="[itemPageRoute, 'bitstreams', 'new']"
[queryParams]="{bundle: bundle.id}" [queryParams]="{bundle: bundle.id}"
class="btn btn-outline-success btn-sm" class="btn btn-outline-success btn-sm"
title="{{'item.edit.bitstreams.bundle.edit.buttons.upload' | translate}}"> 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 { Item } from '../../../../core/shared/item.model';
import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes';
import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes';
import { getItemPageRoute } from '../../../item-page-routing-paths';
@Component({ @Component({
selector: 'ds-item-edit-bitstream-bundle', selector: 'ds-item-edit-bitstream-bundle',
@@ -49,11 +50,17 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
*/ */
bundleNameColumn: ResponsiveColumnSizes; bundleNameColumn: ResponsiveColumnSizes;
/**
* Route to the item's page
*/
itemPageRoute: string;
constructor(private viewContainerRef: ViewContainerRef) { constructor(private viewContainerRef: ViewContainerRef) {
} }
ngOnInit(): void { ngOnInit(): void {
this.bundleNameColumn = this.columnSizes.combineColumns(0, 2); this.bundleNameColumn = this.columnSizes.combineColumns(0, 2);
this.viewContainerRef.createEmbeddedView(this.bundleView); 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 mockCollection = Object.assign(new Collection(), { id: 'collection1' });
const mockItem: Item = Object.assign(new Item(), { const mockItem: Item = Object.assign(new Item(), {
id: '932c7d50-d85a-44cb-b9dc-b427b12877bd', id: '932c7d50-d85a-44cb-b9dc-b427b12877bd',
uuid: '932c7d50-d85a-44cb-b9dc-b427b12877bd',
name: 'test-item' name: 'test-item'
}); });
const mockItemRD: RemoteData<Item> = createSuccessfulRemoteDataObject(mockItem); const mockItemRD: RemoteData<Item> = createSuccessfulRemoteDataObject(mockItem);
@@ -212,7 +213,7 @@ describe('ItemCollectionMapperComponent', () => {
}); });
it('should navigate to the item page', () => { 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 { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
import { SearchService } from '../../../core/shared/search/search.service'; import { SearchService } from '../../../core/shared/search/search.service';
import { NoContent } from '../../../core/shared/NoContent.model'; import { NoContent } from '../../../core/shared/NoContent.model';
import { getItemPageRoute } from '../../item-page-routing-paths';
@Component({ @Component({
selector: 'ds-item-collection-mapper', selector: 'ds-item-collection-mapper',
@@ -312,7 +313,7 @@ export class ItemCollectionMapperComponent implements OnInit {
getRemoteDataPayload(), getRemoteDataPayload(),
take(1) take(1)
).subscribe((item: Item) => { ).subscribe((item: Item) => {
this.router.navigate(['/items/', item.id]); this.router.navigate([getItemPageRoute(item)]);
}); });
} }

View File

@@ -89,7 +89,7 @@
<button (click)="performAction()" <button (click)="performAction()"
class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}} class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}}
</button> </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}} {{cancelMessage| translate}}
</button> </button>

View File

@@ -183,7 +183,7 @@ describe('ItemDeleteComponent', () => {
describe('notify', () => { describe('notify', () => {
it('should navigate to the item edit page on failed deletion of the item', () => { it('should navigate to the item edit page on failed deletion of the item', () => {
comp.notify(false); 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(['']); this.router.navigate(['']);
} else { } else {
this.notificationsService.error(this.translateService.get('item.edit.' + this.messageKey + '.error')); 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] { .btn[disabled] {
color: $gray-600; color: var(--bs-gray-600);
border-color: $gray-600; border-color: var(--bs-gray-600);
z-index: 0; // prevent border colors jumping on hover z-index: 0; // prevent border colors jumping on hover
} }
.metadata-field { .metadata-field {
width: $edit-item-metadata-field-width; width: var(--ds-edit-item-metadata-field-width);
} }
.language-field { .language-field {
width: $edit-item-language-field-width; width: var(--ds-edit-item-language-field-width);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,7 @@
{{'item.edit.tabs.status.labels.itemPage' | translate}}: {{'item.edit.tabs.status.labels.itemPage' | translate}}:
</div> </div>
<div class="col-9 float-left status-data" id="status-itemPage"> <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>
<div *ngFor="let operation of (operations$ | async)" class="w-100" [ngClass]="{'pt-3': operation}"> <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(), { const mockItem = Object.assign(new Item(), {
id: 'fake-id', id: 'fake-id',
uuid: 'fake-id',
handle: 'fake/handle', handle: 'fake/handle',
lastModified: '2018', lastModified: '2018',
_links: { _links: {
@@ -27,7 +28,7 @@ describe('ItemStatusComponent', () => {
} }
}); });
const itemPageUrl = `items/${mockItem.id}`; const itemPageUrl = `/items/${mockItem.uuid}`;
const routeStub = { const routeStub = {
parent: { 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 { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators';
@Component({ @Component({
selector: 'ds-item-status', selector: 'ds-item-status',
@@ -50,6 +51,11 @@ export class ItemStatusComponent implements OnInit {
*/ */
actionsKeys; actionsKeys;
/**
* Route to the item's page
*/
itemPageRoute$: Observable<string>;
constructor(private route: ActivatedRoute, constructor(private route: ActivatedRoute,
private authorizationService: AuthorizationDataService) { private authorizationService: AuthorizationDataService) {
} }
@@ -109,15 +115,10 @@ export class ItemStatusComponent implements OnInit {
}); });
} }
}); });
this.itemPageRoute$ = this.itemRD$.pipe(
} getAllSucceededRemoteDataPayload(),
map((item) => getItemPageRoute(item))
/** );
* Get the url to the simple item page
* @returns {string} url
*/
getItemPage(item: Item): string {
return getItemPageRoute(item.id);
} }
/** /**
@@ -125,7 +126,7 @@ export class ItemStatusComponent implements OnInit {
* @returns {string} url * @returns {string} url
*/ */
getCurrentUrl(item: Item): string { getCurrentUrl(item: Item): string {
return getItemEditRoute(item.id); return getItemEditRoute(item);
} }
trackOperation(index: number, operation: ItemOperation) { trackOperation(index: number, operation: ItemOperation) {

View File

@@ -6,7 +6,7 @@
<ds-modify-item-overview [item]="item"></ds-modify-item-overview> <ds-modify-item-overview [item]="item"></ds-modify-item-overview>
<button (click)="performAction()" class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}} <button (click)="performAction()" class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}}
</button> </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}} {{cancelMessage| translate}}
</button> </button>

View File

@@ -74,9 +74,9 @@ describe('AbstractSimpleItemActionComponent', () => {
routeStub = { routeStub = {
data: observableOf({ data: observableOf({
dso: createSuccessfulRemoteDataObject({ dso: createSuccessfulRemoteDataObject(Object.assign(new Item(), {
id: 'fake-id' id: 'fake-id'
}) }))
}) })
}; };
@@ -136,14 +136,14 @@ describe('AbstractSimpleItemActionComponent', () => {
comp.processRestResponse(successfulRemoteData); comp.processRestResponse(successfulRemoteData);
expect(notificationsServiceStub.success).toHaveBeenCalled(); 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', () => { it('should process a RemoteData to navigate and display success notification', () => {
comp.processRestResponse(failedRemoteData); comp.processRestResponse(failedRemoteData);
expect(notificationsServiceStub.error).toHaveBeenCalled(); 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 { getFirstSucceededRemoteData } from '../../../core/shared/operators';
import { first, map } from 'rxjs/operators'; import { first, map } from 'rxjs/operators';
import { findSuccessfulAccordingTo } from '../edit-item-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. * 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; headerMessage: string;
descriptionMessage: string; descriptionMessage: string;
/**
* Route to the item's page
*/
itemPageRoute: string;
protected predicate: Predicate<RemoteData<Item>>; protected predicate: Predicate<RemoteData<Item>>;
constructor(protected route: ActivatedRoute, constructor(protected route: ActivatedRoute,
@@ -47,6 +52,7 @@ export class AbstractSimpleItemActionComponent implements OnInit {
this.itemRD$.pipe(first()).subscribe((rd) => { this.itemRD$.pipe(first()).subscribe((rd) => {
this.item = rd.payload; 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( this.itemDataService.findById(this.item.id).pipe(
findSuccessfulAccordingTo(this.predicate)).subscribe(() => { findSuccessfulAccordingTo(this.predicate)).subscribe(() => {
this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success')); 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 { } else {
this.notificationsService.error(this.translateService.get('item.edit.' + this.messageKey + '.error')); 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"> <div class="d-flex flex-row">
<ds-item-page-title-field class="mr-auto" [item]="item"></ds-item-page-title-field> <ds-item-page-title-field class="mr-auto" [item]="item"></ds-item-page-title-field>
<div class="pl-2"> <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> </div>
<div class="simple-view-link my-3"> <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}} {{"item.page.link.simple" | translate}}
</a> </a>
</div> </div>

View File

@@ -1,4 +1,6 @@
import { URLCombiner } from '../core/url-combiner/url-combiner'; 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'; export const ITEM_MODULE_PATH = 'items';
@@ -6,12 +8,30 @@ export function getItemModuleRoute() {
return `/${ITEM_MODULE_PATH}`; 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) { export function getItemEditRoute(item: Item) {
return new URLCombiner(getItemModuleRoute(), id, ITEM_EDIT_PATH).toString(); 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'; export const ITEM_EDIT_PATH = 'edit';

View File

@@ -32,17 +32,7 @@ const ENTRY_COMPONENTS = [
UntypedItemComponent UntypedItemComponent
]; ];
@NgModule({ const DECLARATIONS = [
imports: [
CommonModule,
SharedModule.withEntryComponents(),
ItemPageRoutingModule,
EditItemPageModule,
StatisticsModule.forRoot(),
JournalEntitiesModule.withEntryComponents(),
ResearchEntitiesModule.withEntryComponents()
],
declarations: [
ItemPageComponent, ItemPageComponent,
FullItemPageComponent, FullItemPageComponent,
MetadataUriValuesComponent, MetadataUriValuesComponent,
@@ -60,6 +50,23 @@ const ENTRY_COMPONENTS = [
ItemComponent, ItemComponent,
UploadBitstreamComponent, UploadBitstreamComponent,
AbstractIncrementalListComponent, AbstractIncrementalListComponent,
];
@NgModule({
imports: [
CommonModule,
SharedModule.withEntryComponents(),
ItemPageRoutingModule,
EditItemPageModule,
StatisticsModule.forRoot(),
JournalEntitiesModule.withEntryComponents(),
ResearchEntitiesModule.withEntryComponents()
],
declarations: [
...DECLARATIONS
],
exports: [
...DECLARATIONS
] ]
}) })
export class ItemPageModule { export class ItemPageModule {

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; 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 { Observable } from 'rxjs';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { ItemDataService } from '../core/data/item-data.service'; 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 { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model';
import { FindListOptions } from '../core/data/request.models'; import { FindListOptions } from '../core/data/request.models';
import { getFirstCompletedRemoteData } from '../core/shared/operators'; 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>[] = [ 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('bundles', new FindListOptions(), true, true, true, followLink('bitstreams')),
followLink('relationships'), followLink('relationships'),
followLink('version', undefined, true, true, true, followLink('versionhistory')), followLink('version', undefined, true, true, true, followLink('versionhistory')),
@@ -20,7 +32,11 @@ export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Item>[] = [
*/ */
@Injectable() @Injectable()
export class ItemPageResolver implements Resolve<RemoteData<Item>> { 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 * or an error if something went wrong
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
return this.itemService.findById(route.params.id, const itemRD$ = this.itemService.findById(route.params.id,
true, true,
false, false,
...ITEM_PAGE_LINKS_TO_FOLLOW ...ITEM_PAGE_LINKS_TO_FOLLOW
).pipe( ).pipe(
getFirstCompletedRemoteData(), 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 { MetadataService } from '../../core/metadata/metadata.service';
import { fadeInOut } from '../../shared/animations/fade'; 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 { ViewMode } from '../../core/shared/view-mode.model';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { getItemPageRoute } from '../item-page-routing-paths';
/** /**
* This component renders a simple item page. * This component renders a simple item page.
@@ -44,6 +45,11 @@ export class ItemPageComponent implements OnInit {
*/ */
viewMode = ViewMode.StandalonePage; viewMode = ViewMode.StandalonePage;
/**
* Route to the item's page
*/
itemPageRoute$: Observable<string>;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
@@ -61,5 +67,9 @@ export class ItemPageComponent implements OnInit {
redirectOn4xx(this.router, this.authService) redirectOn4xx(this.router, this.authService)
); );
this.metadataService.processRemoteData(this.itemRD$); 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> {{'publication.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
</h2> </h2>
<div class="pl-2"> <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> </div>
<div class="row"> <div class="row">
@@ -74,7 +74,7 @@
</ds-item-page-uri-field> </ds-item-page-uri-field>
<ds-item-page-collections [item]="object"></ds-item-page-collections> <ds-item-page-collections [item]="object"></ds-item-page-collections>
<div> <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}} {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

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

View File

@@ -3,7 +3,7 @@
<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values> <ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
</h2> </h2>
<div class="pl-2"> <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> </div>
<div class="row"> <div class="row">
@@ -59,7 +59,7 @@
</ds-item-page-uri-field> </ds-item-page-uri-field>
<ds-item-page-collections [item]="object"></ds-item-page-collections> <ds-item-page-collections [item]="object"></ds-item-page-collections>
<div> <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}} {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

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

View File

@@ -21,6 +21,10 @@ import { UploaderService } from '../../shared/uploader/uploader.service';
import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { UploaderComponent } from '../../shared/uploader/uploader.component'; import { UploaderComponent } from '../../shared/uploader/uploader.component';
import { HttpXsrfTokenExtractor } from '@angular/common/http';
import { CookieService } from '../../core/services/cookie.service';
import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock';
import { HttpXsrfTokenExtractorMock } from '../../shared/mocks/http-xsrf-token-extractor.mock';
describe('MyDSpaceNewSubmissionComponent test', () => { describe('MyDSpaceNewSubmissionComponent test', () => {
@@ -55,6 +59,8 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
ChangeDetectorRef, ChangeDetectorRef,
MyDSpaceNewSubmissionComponent, MyDSpaceNewSubmissionComponent,
UploaderService, UploaderService,
{ provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock('mock-token') },
{ provide: CookieService, useValue: new CookieServiceMock() },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]

View File

@@ -6,6 +6,7 @@
[configurationList]="(configurationList$ | async)" [configurationList]="(configurationList$ | async)"
[resultCount]="(resultsRD$ | async)?.payload.totalElements" [resultCount]="(resultsRD$ | async)?.payload.totalElements"
[viewModeList]="viewModeList" [viewModeList]="viewModeList"
[refreshFilters]="refreshFilters.asObservable()"
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar> [inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
<div class="col-12 col-md-9"> <div class="col-12 col-md-9">
<ds-search-form id="search-form" <ds-search-form id="search-form"
@@ -26,6 +27,7 @@
[resultCount]="(resultsRD$ | async)?.payload.totalElements" [resultCount]="(resultsRD$ | async)?.payload.totalElements"
(toggleSidebar)="closeSidebar()" (toggleSidebar)="closeSidebar()"
[ngClass]="{'active': !(isSidebarCollapsed() | async)}" [ngClass]="{'active': !(isSidebarCollapsed() | async)}"
[refreshFilters]="refreshFilters.asObservable()"
[inPlaceSearch]="inPlaceSearch"> [inPlaceSearch]="inPlaceSearch">
</ds-search-sidebar> </ds-search-sidebar>
<div id="search-content" class="col-12"> <div id="search-content" class="col-12">
@@ -39,7 +41,8 @@
</div> </div>
<ds-my-dspace-results [searchResults]="resultsRD$ | async" <ds-my-dspace-results [searchResults]="resultsRD$ | async"
[searchConfig]="searchOptions$ | async" [searchConfig]="searchOptions$ | async"
[context]="context$ | async"></ds-my-dspace-results> [context]="context$ | async"
(contentChange)="onResultsContentChange()"></ds-my-dspace-results>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,7 +7,7 @@ import {
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
import { map, switchMap, tap, } from 'rxjs/operators'; import { map, switchMap, tap, } from 'rxjs/operators';
import { PaginatedList } from '../core/data/paginated-list.model'; import { PaginatedList } from '../core/data/paginated-list.model';
@@ -101,6 +101,11 @@ export class MyDSpacePageComponent implements OnInit {
*/ */
context$: Observable<Context>; context$: Observable<Context>;
/**
* Emit an event every time search sidebars must refresh their contents.
*/
refreshFilters: Subject<any> = new Subject<any>();
constructor(private service: SearchService, constructor(private service: SearchService,
private sidebarService: SidebarService, private sidebarService: SidebarService,
private windowService: HostWindowService, private windowService: HostWindowService,
@@ -148,6 +153,14 @@ export class MyDSpacePageComponent implements OnInit {
} }
/**
* Handle the contentChange event from within the my dspace content.
* Notify search sidebars to refresh their content.
*/
onResultsContentChange() {
this.refreshFilters.next();
}
/** /**
* Set the sidebar to a collapsed state * Set the sidebar to a collapsed state
*/ */
@@ -184,5 +197,6 @@ export class MyDSpacePageComponent implements OnInit {
if (hasValue(this.sub)) { if (hasValue(this.sub)) {
this.sub.unsubscribe(); this.sub.unsubscribe();
} }
this.refreshFilters.complete();
} }
} }

View File

@@ -5,7 +5,8 @@
[sortConfig]="searchConfig.sort" [sortConfig]="searchConfig.sort"
[objects]="searchResults" [objects]="searchResults"
[hideGear]="true" [hideGear]="true"
[context]="context"> [context]="context"
(contentChange)="contentChange.emit()">
</ds-viewable-collection> </ds-viewable-collection>
</div> </div>
<ds-loading *ngIf="isLoading()" message="{{'loading.mydspace-results' | translate}}"></ds-loading> <ds-loading *ngIf="isLoading()" message="{{'loading.mydspace-results' | translate}}"></ds-loading>

View File

@@ -1,4 +1,4 @@
import { Component, Input } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { fadeIn, fadeInOut } from '../../shared/animations/fade'; import { fadeIn, fadeInOut } from '../../shared/animations/fade';
@@ -41,6 +41,12 @@ export class MyDSpaceResultsComponent {
* The current context for the search results * The current context for the search results
*/ */
@Input() context: Context; @Input() context: Context;
/**
* Emit when one of the results has changed.
*/
@Output() contentChange = new EventEmitter<any>();
/** /**
* A boolean representing if search results entry are separated by a line * A boolean representing if search results entry are separated by a line
*/ */

View File

@@ -14,12 +14,16 @@ import { ClaimedTaskSearchResultDetailElementComponent } from '../shared/object-
import { ItemSearchResultListElementSubmissionComponent } from '../shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component'; import { ItemSearchResultListElementSubmissionComponent } from '../shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component';
import { WorkflowItemSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component'; import { WorkflowItemSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component';
import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component'; import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component';
import { ClaimedApprovedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component';
import { ClaimedDeclinedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component';
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator // put only entry components that use custom decorator
WorkspaceItemSearchResultListElementComponent, WorkspaceItemSearchResultListElementComponent,
WorkflowItemSearchResultListElementComponent, WorkflowItemSearchResultListElementComponent,
ClaimedSearchResultListElementComponent, ClaimedSearchResultListElementComponent,
ClaimedApprovedSearchResultListElementComponent,
ClaimedDeclinedSearchResultListElementComponent,
PoolSearchResultListElementComponent, PoolSearchResultListElementComponent,
ItemSearchResultDetailElementComponent, ItemSearchResultDetailElementComponent,
WorkspaceItemSearchResultDetailElementComponent, WorkspaceItemSearchResultDetailElementComponent,

View File

@@ -1,7 +1,14 @@
import { HostWindowService } from '../shared/host-window.service'; import { HostWindowService } from '../shared/host-window.service';
import { SidebarService } from '../shared/sidebar/sidebar.service'; import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SearchComponent } from './search.component'; import { SearchComponent } from './search.component';
import { ChangeDetectionStrategy, Component, Inject, Input, OnDestroy, OnInit } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
Inject,
Input,
OnDestroy,
OnInit
} from '@angular/core';
import { pushInOut } from '../shared/animations/push'; import { pushInOut } from '../shared/animations/push';
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';

View File

@@ -6,5 +6,5 @@
} }
::ng-deep .search-controls { ::ng-deep .search-controls {
margin-bottom: $spacer; margin-bottom: var(--bs-spacer);
} }

View File

@@ -7,7 +7,8 @@ import {
of as observableOf, of as observableOf,
Subscription, Subscription,
BehaviorSubject, BehaviorSubject,
combineLatest as observableCombineLatest, ObservedValueOf, combineLatest as observableCombineLatest,
ObservedValueOf,
} from 'rxjs'; } from 'rxjs';
import { map, mergeMap, switchMap, take } from 'rxjs/operators'; import { map, mergeMap, switchMap, take } from 'rxjs/operators';
import {buildPaginatedList, PaginatedList} from '../../../../core/data/paginated-list.model'; import {buildPaginatedList, PaginatedList} from '../../../../core/data/paginated-list.model';

View File

@@ -5,6 +5,7 @@ import { Item } from './core/shared/item.model';
import { getCommunityPageRoute } from './+community-page/community-page-routing-paths'; import { getCommunityPageRoute } from './+community-page/community-page-routing-paths';
import { getCollectionPageRoute } from './+collection-page/collection-page-routing-paths'; import { getCollectionPageRoute } from './+collection-page/collection-page-routing-paths';
import { getItemPageRoute } from './+item-page/item-page-routing-paths'; import { getItemPageRoute } from './+item-page/item-page-routing-paths';
import { hasValue } from './shared/empty.util';
export const BITSTREAM_MODULE_PATH = 'bitstreams'; export const BITSTREAM_MODULE_PATH = 'bitstreams';
@@ -45,13 +46,15 @@ export function getWorkflowItemModuleRoute() {
} }
export function getDSORoute(dso: DSpaceObject): string { export function getDSORoute(dso: DSpaceObject): string {
if (hasValue(dso)) {
switch ((dso as any).type) { switch ((dso as any).type) {
case Community.type.value: case Community.type.value:
return getCommunityPageRoute(dso.uuid); return getCommunityPageRoute(dso.uuid);
case Collection.type.value: case Collection.type.value:
return getCollectionPageRoute(dso.uuid); return getCollectionPageRoute(dso.uuid);
case Item.type.value: case Item.type.value:
return getItemPageRoute(dso.uuid); return getItemPageRoute(dso as Item);
}
} }
} }

View File

@@ -88,6 +88,11 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
.then((m) => m.ItemPageModule), .then((m) => m.ItemPageModule),
canActivate: [EndUserAgreementCurrentUserGuard] canActivate: [EndUserAgreementCurrentUserGuard]
}, },
{ path: 'entities/:entity-type',
loadChildren: () => import('./+item-page/item-page.module')
.then((m) => m.ItemPageModule),
canActivate: [EndUserAgreementCurrentUserGuard]
},
{ {
path: BITSTREAM_MODULE_PATH, path: BITSTREAM_MODULE_PATH,
loadChildren: () => import('./+bitstream-page/bitstream-page.module') loadChildren: () => import('./+bitstream-page/bitstream-page.module')

View File

@@ -1,30 +1 @@
<div class="outer-wrapper" *ngIf="isNotAuthBlocking$ | async; else authLoader"> <ds-themed-root [isNotAuthBlocking]="isNotAuthBlocking$ | async" [isLoading]="isLoading$ | async"></ds-themed-root>
<ds-admin-sidebar></ds-admin-sidebar>
<div class="inner-wrapper" [@slideSidebarPadding]="{
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
params: {collapsedSidebarWidth: (collapsedSidebarWidth | async), totalSidebarWidth: (totalSidebarWidth | async)}
}">
<ds-header-navbar-wrapper></ds-header-navbar-wrapper>
<ds-notifications-board
[options]="notificationOptions">
</ds-notifications-board>
<main class="main-content">
<div class="container">
<ds-breadcrumbs></ds-breadcrumbs>
</div>
<div class="container" *ngIf="isLoading$ | async">
<ds-loading message="{{'loading.default' | translate}}"></ds-loading>
</div>
<router-outlet></router-outlet>
</main>
<ds-footer></ds-footer>
</div>
</div>
<ng-template #authLoader>
<div class="text-center ds-full-screen-loader d-flex align-items-center flex-column justify-content-center">
<ds-loading [showMessage]="false"></ds-loading>
</div>
</ng-template>

View File

@@ -1,53 +0,0 @@
@import '../styles/helpers/font_awesome_imports.scss';
@import '../../node_modules/bootstrap/scss/bootstrap.scss';
@import '../../node_modules/nouislider/distribute/nouislider.min';
html {
position: relative;
min-height: 100%;
}
body {
overflow-x: hidden;
}
// Sticky Footer
.outer-wrapper {
display: flex;
margin: 0;
}
.inner-wrapper {
flex: 1 1 auto;
flex-flow: column nowrap;
display: flex;
min-height: 100vh;
flex-direction: column;
width: 100%;
position: relative;
}
.main-content {
z-index: $main-z-index;
flex: 1 1 100%;
margin-top: $content-spacing;
margin-bottom: $content-spacing;
}
.alert.hide {
padding: 0;
margin: 0;
}
ds-header-navbar-wrapper {
z-index: $nav-z-index;
}
ds-admin-sidebar {
position: fixed;
z-index: $sidebar-z-index;
}
.ds-full-screen-loader {
height: 100vh;
}

View File

@@ -1,7 +1,7 @@
import { Store, StoreModule } from '@ngrx/store'; import { Store, StoreModule } from '@ngrx/store';
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule, DOCUMENT } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
@@ -33,6 +33,8 @@ import { LocaleService } from './core/locale/locale.service';
import { authReducer } from './core/auth/auth.reducer'; import { authReducer } from './core/auth/auth.reducer';
import { provideMockStore } from '@ngrx/store/testing'; import { provideMockStore } from '@ngrx/store/testing';
import { GoogleAnalyticsService } from './statistics/google-analytics.service'; import { GoogleAnalyticsService } from './statistics/google-analytics.service';
import { ThemeService } from './shared/theme-support/theme.service';
import { getMockThemeService } from './shared/mocks/theme-service.mock';
let comp: AppComponent; let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>; let fixture: ComponentFixture<AppComponent>;
@@ -73,6 +75,7 @@ describe('App component', () => {
{ provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: CSSVariableService, useClass: CSSVariableServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
{ provide: LocaleService, useValue: getMockLocaleService() }, { provide: LocaleService, useValue: getMockLocaleService() },
{ provide: ThemeService, useValue: getMockThemeService() },
provideMockStore({ initialState }), provideMockStore({ initialState }),
AppComponent, AppComponent,
RouteService RouteService
@@ -143,4 +146,32 @@ describe('App component', () => {
}); });
}); });
}); });
describe('when ThemeService returns a custom theme', () => {
let document;
let headSpy;
beforeEach(() => {
// NOTE: Cannot override providers once components have been compiled, so TestBed needs to be reset
TestBed.resetTestingModule();
TestBed.configureTestingModule(defaultTestBedConf);
TestBed.overrideProvider(ThemeService, {useValue: getMockThemeService('custom')});
document = TestBed.inject(DOCUMENT);
headSpy = jasmine.createSpyObj('head', ['appendChild']);
spyOn(document, 'getElementsByTagName').and.returnValue([headSpy]);
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
});
it('should append a link element with the correct attributes to the head element', () => {
const link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('type', 'text/css');
link.setAttribute('class', 'theme-css');
link.setAttribute('href', '/custom-theme.css');
expect(headSpy.appendChild).toHaveBeenCalledWith(link);
});
});
}); });

View File

@@ -5,12 +5,12 @@ import {
Component, Component,
HostListener, HostListener,
Inject, Inject,
OnInit, Optional, OnInit,
ViewEncapsulation Optional,
} from '@angular/core'; } from '@angular/core';
import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router'; import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router';
import { BehaviorSubject, combineLatest as combineLatestObservable, Observable, of } from 'rxjs'; import { BehaviorSubject, Observable, of } from 'rxjs';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
@@ -23,25 +23,25 @@ import { isAuthenticationBlocking } from './core/auth/selectors';
import { AuthService } from './core/auth/auth.service'; import { AuthService } from './core/auth/auth.service';
import { CSSVariableService } from './shared/sass-helper/sass-helper.service'; import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
import { MenuService } from './shared/menu/menu.service'; import { MenuService } from './shared/menu/menu.service';
import { MenuID } from './shared/menu/initial-menus-state';
import { slideSidebarPadding } from './shared/animations/slide';
import { HostWindowService } from './shared/host-window.service'; import { HostWindowService } from './shared/host-window.service';
import { Theme } from '../config/theme.inferface'; import { ThemeConfig } from '../config/theme.model';
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { models } from './core/core.module'; import { models } from './core/core.module';
import { LocaleService } from './core/locale/locale.service'; import { LocaleService } from './core/locale/locale.service';
import { hasValue } from './shared/empty.util'; import { hasValue, isNotEmpty } from './shared/empty.util';
import { KlaroService } from './shared/cookies/klaro.service'; import { KlaroService } from './shared/cookies/klaro.service';
import { GoogleAnalyticsService } from './statistics/google-analytics.service'; import { GoogleAnalyticsService } from './statistics/google-analytics.service';
import { DOCUMENT } from '@angular/common';
import { ThemeService } from './shared/theme-support/theme.service';
import { BASE_THEME_NAME } from './shared/theme-support/theme.constants';
import { DEFAULT_THEME_CONFIG } from './shared/theme-support/theme.effects';
@Component({ @Component({
selector: 'ds-app', selector: 'ds-app',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'], styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
animations: [slideSidebarPadding]
}) })
export class AppComponent implements OnInit, AfterViewInit { export class AppComponent implements OnInit, AfterViewInit {
isLoading$: BehaviorSubject<boolean> = new BehaviorSubject(true); isLoading$: BehaviorSubject<boolean> = new BehaviorSubject(true);
@@ -49,7 +49,7 @@ export class AppComponent implements OnInit, AfterViewInit {
slideSidebarOver: Observable<boolean>; slideSidebarOver: Observable<boolean>;
collapsedSidebarWidth: Observable<string>; collapsedSidebarWidth: Observable<string>;
totalSidebarWidth: Observable<string>; totalSidebarWidth: Observable<string>;
theme: Observable<Theme> = of({} as any); theme: Observable<ThemeConfig> = of({} as any);
notificationOptions = environment.notifications; notificationOptions = environment.notifications;
models; models;
@@ -60,6 +60,8 @@ export class AppComponent implements OnInit, AfterViewInit {
constructor( constructor(
@Inject(NativeWindowService) private _window: NativeWindowRef, @Inject(NativeWindowService) private _window: NativeWindowRef,
@Inject(DOCUMENT) private document: any,
private themeService: ThemeService,
private translate: TranslateService, private translate: TranslateService,
private store: Store<HostWindowState>, private store: Store<HostWindowState>,
private metadata: MetadataService, private metadata: MetadataService,
@@ -77,6 +79,17 @@ export class AppComponent implements OnInit, AfterViewInit {
/* Use models object so all decorators are actually called */ /* Use models object so all decorators are actually called */
this.models = models; this.models = models;
this.themeService.getThemeName$().subscribe((themeName: string) => {
if (hasValue(themeName)) {
this.setThemeCss(themeName);
} else if (hasValue(DEFAULT_THEME_CONFIG)) {
this.setThemeCss(DEFAULT_THEME_CONFIG.name);
} else {
this.setThemeCss(BASE_THEME_NAME);
}
});
// Load all the languages that are defined as active from the config file // Load all the languages that are defined as active from the config file
translate.addLangs(environment.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code)); translate.addLangs(environment.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code));
@@ -116,17 +129,6 @@ export class AppComponent implements OnInit, AfterViewInit {
const color: string = environment.production ? 'red' : 'green'; const color: string = environment.production ? 'red' : 'green';
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`); console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight); this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight);
this.sidebarVisible = this.menuService.isMenuVisible(MenuID.ADMIN);
this.collapsedSidebarWidth = this.cssService.getVariable('collapsedSidebarWidth');
this.totalSidebarWidth = this.cssService.getVariable('totalSidebarWidth');
const sidebarCollapsed = this.menuService.isMenuCollapsed(MenuID.ADMIN);
this.slideSidebarOver = combineLatestObservable(sidebarCollapsed, this.windowService.isXsOrSm())
.pipe(
map(([collapsed, mobile]) => collapsed || mobile)
);
} }
private storeCSSVariables() { private storeCSSVariables() {
@@ -177,4 +179,34 @@ export class AppComponent implements OnInit, AfterViewInit {
this.cookiesService.initialize(); this.cookiesService.initialize();
} }
} }
/**
* Update the theme css file in <head>
*
* @param themeName The name of the new theme
* @private
*/
private setThemeCss(themeName: string): void {
const head = this.document.getElementsByTagName('head')[0];
// Array.from to ensure we end up with an array, not an HTMLCollection, which would be
// automatically updated if we add nodes later
const currentThemeLinks = Array.from(this.document.getElementsByClassName('theme-css'));
const link = this.document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('type', 'text/css');
link.setAttribute('class', 'theme-css');
link.setAttribute('href', `/${encodeURIComponent(themeName)}-theme.css`);
// wait for the new css to download before removing the old one to prevent a
// flash of unstyled content
link.onload = () => {
if (isNotEmpty(currentThemeLinks)) {
currentThemeLinks.forEach((currentThemeLink: any) => {
if (hasValue(currentThemeLink)) {
currentThemeLink.remove();
}
});
}
};
head.appendChild(link);
}
} }

View File

@@ -3,11 +3,13 @@ import { NotificationsEffects } from './shared/notifications/notifications.effec
import { NavbarEffects } from './navbar/navbar.effects'; import { NavbarEffects } from './navbar/navbar.effects';
import { SidebarEffects } from './shared/sidebar/sidebar-effects.service'; import { SidebarEffects } from './shared/sidebar/sidebar-effects.service';
import { RelationshipEffects } from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects'; import { RelationshipEffects } from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects';
import { ThemeEffects } from './shared/theme-support/theme.effects';
export const appEffects = [ export const appEffects = [
StoreEffects, StoreEffects,
NavbarEffects, NavbarEffects,
NotificationsEffects, NotificationsEffects,
SidebarEffects, SidebarEffects,
ThemeEffects,
RelationshipEffects RelationshipEffects
]; ];

View File

@@ -43,6 +43,9 @@ import { ForbiddenComponent } from './forbidden/forbidden.component';
import { AuthInterceptor } from './core/auth/auth.interceptor'; import { AuthInterceptor } from './core/auth/auth.interceptor';
import { LocaleInterceptor } from './core/locale/locale.interceptor'; import { LocaleInterceptor } from './core/locale/locale.interceptor';
import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor'; import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
import { RootComponent } from './root/root.component';
import { ThemedRootComponent } from './root/themed-root.component';
import { ThemedEntryComponentModule } from '../themes/themed-entry-component.module';
export function getBase() { export function getBase() {
return environment.ui.nameSpace; return environment.ui.nameSpace;
@@ -65,6 +68,7 @@ const IMPORTS = [
EffectsModule.forRoot(appEffects), EffectsModule.forRoot(appEffects),
StoreModule.forRoot(appReducers, storeModuleConfig), StoreModule.forRoot(appReducers, storeModuleConfig),
StoreRouterConnectingModule.forRoot(), StoreRouterConnectingModule.forRoot(),
ThemedEntryComponentModule.withEntryComponents(),
]; ];
IMPORTS.push( IMPORTS.push(
@@ -120,6 +124,8 @@ const PROVIDERS = [
const DECLARATIONS = [ const DECLARATIONS = [
AppComponent, AppComponent,
RootComponent,
ThemedRootComponent,
HeaderComponent, HeaderComponent,
HeaderNavbarWrapperComponent, HeaderNavbarWrapperComponent,
AdminSidebarComponent, AdminSidebarComponent,
@@ -135,7 +141,6 @@ const DECLARATIONS = [
]; ];
const EXPORTS = [ const EXPORTS = [
AppComponent
]; ];
@NgModule({ @NgModule({
@@ -150,7 +155,8 @@ const EXPORTS = [
...DECLARATIONS, ...DECLARATIONS,
], ],
exports: [ exports: [
...EXPORTS ...EXPORTS,
...DECLARATIONS,
] ]
}) })
export class AppModule { export class AppModule {

View File

@@ -12,7 +12,10 @@ import {
metadataRegistryReducer, metadataRegistryReducer,
MetadataRegistryState MetadataRegistryState
} from './+admin/admin-registries/metadata-registry/metadata-registry.reducers'; } from './+admin/admin-registries/metadata-registry/metadata-registry.reducers';
import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer'; import {
CommunityListReducer,
CommunityListState
} from './community-list-page/community-list.reducer';
import { hasValue } from './shared/empty.util'; import { hasValue } from './shared/empty.util';
import { import {
NameVariantListsState, NameVariantListsState,
@@ -20,19 +23,32 @@ import {
} from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer'; } from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
import { formReducer, FormState } from './shared/form/form.reducer'; import { formReducer, FormState } from './shared/form/form.reducer';
import { menusReducer, MenusState } from './shared/menu/menu.reducer'; import { menusReducer, MenusState } from './shared/menu/menu.reducer';
import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers'; import {
notificationsReducer,
NotificationsState
} from './shared/notifications/notifications.reducers';
import { import {
selectableListReducer, selectableListReducer,
SelectableListsState SelectableListsState
} from './shared/object-list/selectable-list/selectable-list.reducer'; } from './shared/object-list/selectable-list/selectable-list.reducer';
import { ObjectSelectionListState, objectSelectionReducer } from './shared/object-select/object-select.reducer'; import {
ObjectSelectionListState,
objectSelectionReducer
} from './shared/object-select/object-select.reducer';
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer'; import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer'; import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
import { filterReducer, SearchFiltersState } from './shared/search/search-filters/search-filter/search-filter.reducer'; import {
import { sidebarFilterReducer, SidebarFiltersState } from './shared/sidebar/filter/sidebar-filter.reducer'; filterReducer,
SearchFiltersState
} from './shared/search/search-filters/search-filter/search-filter.reducer';
import {
sidebarFilterReducer,
SidebarFiltersState
} from './shared/sidebar/filter/sidebar-filter.reducer';
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer'; import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer'; import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
import { ThemeState, themeReducer } from './shared/theme-support/theme.reducer';
export interface AppState { export interface AppState {
router: fromRouter.RouterReducerState; router: fromRouter.RouterReducerState;
@@ -45,6 +61,7 @@ export interface AppState {
searchFilter: SearchFiltersState; searchFilter: SearchFiltersState;
truncatable: TruncatablesState; truncatable: TruncatablesState;
cssVariables: CSSVariablesState; cssVariables: CSSVariablesState;
theme: ThemeState;
menus: MenusState; menus: MenusState;
objectSelection: ObjectSelectionListState; objectSelection: ObjectSelectionListState;
selectableLists: SelectableListsState; selectableLists: SelectableListsState;
@@ -65,6 +82,7 @@ export const appReducers: ActionReducerMap<AppState> = {
searchFilter: filterReducer, searchFilter: filterReducer,
truncatable: truncatableReducer, truncatable: truncatableReducer,
cssVariables: cssVariablesReducer, cssVariables: cssVariablesReducer,
theme: themeReducer,
menus: menusReducer, menus: menusReducer,
objectSelection: objectSelectionReducer, objectSelection: objectSelectionReducer,
selectableLists: selectableListReducer, selectableLists: selectableListReducer,

View File

@@ -19,6 +19,7 @@ import { CommunityListState } from './community-list.reducer';
import { getCommunityPageRoute } from '../+community-page/community-page-routing-paths'; import { getCommunityPageRoute } from '../+community-page/community-page-routing-paths';
import { getCollectionPageRoute } from '../+collection-page/collection-page-routing-paths'; import { getCollectionPageRoute } from '../+collection-page/collection-page-routing-paths';
import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../core/shared/operators'; import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../core/shared/operators';
import { followLink } from '../shared/utils/follow-link-config.model';
/** /**
* Each node in the tree is represented by a flatNode which contains info about the node itself and its position and * Each node in the tree is represented by a flatNode which contains info about the node itself and its position and
@@ -101,7 +102,7 @@ const communityListStateSelector = (state: AppState) => state.communityList;
const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes); const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes);
const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode); const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode);
export const MAX_COMCOLS_PER_PAGE = 50; export const MAX_COMCOLS_PER_PAGE = 20;
/** /**
* Service class for the community list, responsible for the creating of the flat list used by communityList dataSource * Service class for the community list, responsible for the creating of the flat list used by communityList dataSource
@@ -115,6 +116,10 @@ export class CommunityListService {
private store: Store<any>) { private store: Store<any>) {
} }
private configOnePage: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: 1
});
saveCommunityListStateToStore(expandedNodes: FlatNode[], loadingNode: FlatNode): void { saveCommunityListStateToStore(expandedNodes: FlatNode[], loadingNode: FlatNode): void {
this.store.dispatch(new CommunityListSaveAction(expandedNodes, loadingNode)); this.store.dispatch(new CommunityListSaveAction(expandedNodes, loadingNode));
} }
@@ -168,7 +173,10 @@ export class CommunityListService {
field: options.sort.field, field: options.sort.field,
direction: options.sort.direction direction: options.sort.direction
} }
}).pipe( },
followLink('subcommunities', this.configOnePage, true, true),
followLink('collections', this.configOnePage, true, true))
.pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
map((results) => results.payload), map((results) => results.payload),
); );
@@ -233,7 +241,9 @@ export class CommunityListService {
const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, { const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, {
elementsPerPage: MAX_COMCOLS_PER_PAGE, elementsPerPage: MAX_COMCOLS_PER_PAGE,
currentPage: i currentPage: i
}) },
followLink('subcommunities', this.configOnePage, true, true),
followLink('collections', this.configOnePage, true, true))
.pipe( .pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
switchMap((rd: RemoteData<PaginatedList<Community>>) => { switchMap((rd: RemoteData<PaginatedList<Community>>) => {
@@ -289,7 +299,7 @@ export class CommunityListService {
public getIsExpandable(community: Community): Observable<boolean> { public getIsExpandable(community: Community): Observable<boolean> {
let hasSubcoms$: Observable<boolean>; let hasSubcoms$: Observable<boolean>;
let hasColls$: Observable<boolean>; let hasColls$: Observable<boolean>;
hasSubcoms$ = this.communityDataService.findByParent(community.uuid, { elementsPerPage: 1 }) hasSubcoms$ = this.communityDataService.findByParent(community.uuid, this.configOnePage)
.pipe( .pipe(
map((rd: RemoteData<PaginatedList<Community>>) => { map((rd: RemoteData<PaginatedList<Community>>) => {
if (hasValue(rd) && hasValue(rd.payload)) { if (hasValue(rd) && hasValue(rd.payload)) {
@@ -300,7 +310,7 @@ export class CommunityListService {
}), }),
); );
hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 }) hasColls$ = this.collectionDataService.findByParent(community.uuid, this.configOnePage)
.pipe( .pipe(
map((rd: RemoteData<PaginatedList<Collection>>) => { map((rd: RemoteData<PaginatedList<Collection>>) => {
if (hasValue(rd) && hasValue(rd.payload)) { if (hasValue(rd) && hasValue(rd.payload)) {

View File

@@ -3,7 +3,8 @@ import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
import { Collection } from '../shared/collection.model'; import { Collection } from '../shared/collection.model';
import { CollectionDataService } from '../data/collection-data.service'; import { CollectionDataService } from '../data/collection-data.service';
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../+collection-page/collection-page.resolver';
/** /**
* The class that resolves the BreadcrumbConfig object for a Collection * The class that resolves the BreadcrumbConfig object for a Collection
@@ -22,10 +23,6 @@ export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver<Collecti
* Requesting them as embeds will limit the number of requests * Requesting them as embeds will limit the number of requests
*/ */
get followLinks(): FollowLinkConfig<Collection>[] { get followLinks(): FollowLinkConfig<Collection>[] {
return [ return COLLECTION_PAGE_LINKS_TO_FOLLOW;
followLink('parentCommunity', undefined, true, true, true,
followLink('parentCommunity')
)
];
} }
} }

View File

@@ -3,7 +3,8 @@ import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
import { CommunityDataService } from '../data/community-data.service'; import { CommunityDataService } from '../data/community-data.service';
import { Community } from '../shared/community.model'; import { Community } from '../shared/community.model';
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { COMMUNITY_PAGE_LINKS_TO_FOLLOW } from '../../+community-page/community-page.resolver';
/** /**
* The class that resolves the BreadcrumbConfig object for a Community * The class that resolves the BreadcrumbConfig object for a Community
@@ -22,8 +23,6 @@ export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver<Community
* Requesting them as embeds will limit the number of requests * Requesting them as embeds will limit the number of requests
*/ */
get followLinks(): FollowLinkConfig<Community>[] { get followLinks(): FollowLinkConfig<Community>[] {
return [ return COMMUNITY_PAGE_LINKS_TO_FOLLOW;
followLink('parentCommunity')
];
} }
} }

View File

@@ -28,7 +28,7 @@ export class DSOBreadcrumbsService implements BreadcrumbsService<ChildHALResourc
/** /**
* Method to recursively calculate the breadcrumbs * Method to recursively calculate the breadcrumbs
* This method returns the name and url of the key and all its parent DSO's recursively, top down * This method returns the name and url of the key and all its parent DSOs recursively, top down
* @param key The key (a DSpaceObject) used to resolve the breadcrumb * @param key The key (a DSpaceObject) used to resolve the breadcrumb
* @param url The url to use as a link for this breadcrumb * @param url The url to use as a link for this breadcrumb
*/ */

View File

@@ -20,7 +20,7 @@ export class DSONameService {
* *
* With only two exceptions those solutions seem overkill for now. * With only two exceptions those solutions seem overkill for now.
*/ */
private factories = { private readonly factories = {
Person: (dso: DSpaceObject): string => { Person: (dso: DSpaceObject): string => {
return `${dso.firstMetadataValue('person.familyName')}, ${dso.firstMetadataValue('person.givenName')}`; return `${dso.firstMetadataValue('person.familyName')}, ${dso.firstMetadataValue('person.givenName')}`;
}, },

View File

@@ -3,7 +3,8 @@ import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
import { ItemDataService } from '../data/item-data.service'; import { ItemDataService } from '../data/item-data.service';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../+item-page/item-page.resolver';
/** /**
* The class that resolves the BreadcrumbConfig object for an Item * The class that resolves the BreadcrumbConfig object for an Item
@@ -22,13 +23,6 @@ export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver<Item> {
* Requesting them as embeds will limit the number of requests * Requesting them as embeds will limit the number of requests
*/ */
get followLinks(): FollowLinkConfig<Item>[] { get followLinks(): FollowLinkConfig<Item>[] {
return [ return ITEM_PAGE_LINKS_TO_FOLLOW;
followLink('owningCollection', undefined, true, true, true,
followLink('parentCommunity', undefined, true, true, true,
followLink('parentCommunity'))
),
followLink('bundles'),
followLink('relationships')
];
} }
} }

View File

@@ -3,7 +3,15 @@ import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { GenericConstructor } from '../../shared/generic-constructor'; import { GenericConstructor } from '../../shared/generic-constructor';
import { HALResource } from '../../shared/hal-resource.model'; import { HALResource } from '../../shared/hal-resource.model';
import { getDataServiceFor, getLinkDefinition, getLinkDefinitions, LinkDefinition } from './build-decorators'; import {
getDataServiceFor,
getLinkDefinition,
getLinkDefinitions,
LinkDefinition
} from './build-decorators';
import { RemoteData } from '../../data/remote-data';
import { Observable } from 'rxjs/internal/Observable';
import { EMPTY } from 'rxjs';
/** /**
* A Service to handle the resolving and removing * A Service to handle the resolving and removing
@@ -33,12 +41,14 @@ export class LinkService {
} }
/** /**
* Resolve the given {@link FollowLinkConfig} for the given model * Resolve the given {@link FollowLinkConfig} for the given model and return the result. This does
* not attach the link result to the property on the model. Useful when you're working with a
* readonly object
* *
* @param model the {@link HALResource} to resolve the link for * @param model the {@link HALResource} to resolve the link for
* @param linkToFollow the {@link FollowLinkConfig} to resolve * @param linkToFollow the {@link FollowLinkConfig} to resolve
*/ */
public resolveLink<T extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): T { public resolveLinkWithoutAttaching<T extends HALResource, U extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): Observable<RemoteData<U>> {
const matchingLinkDef = getLinkDefinition(model.constructor, linkToFollow.name); const matchingLinkDef = getLinkDefinition(model.constructor, linkToFollow.name);
if (hasNoValue(matchingLinkDef)) { if (hasNoValue(matchingLinkDef)) {
@@ -61,9 +71,9 @@ export class LinkService {
try { try {
if (matchingLinkDef.isList) { if (matchingLinkDef.isList) {
model[linkToFollow.name] = service.findAllByHref(href, linkToFollow.findListOptions, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow); return service.findAllByHref(href, linkToFollow.findListOptions, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow);
} else { } else {
model[linkToFollow.name] = service.findByHref(href, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow); return service.findByHref(href, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow);
} }
} catch (e) { } catch (e) {
console.error(`Something went wrong when using @dataService(${matchingLinkDef.resourceType.value}) ${hasValue(service) ? '' : '(undefined) '}to resolve link ${linkToFollow.name} at ${href}`); console.error(`Something went wrong when using @dataService(${matchingLinkDef.resourceType.value}) ${hasValue(service) ? '' : '(undefined) '}to resolve link ${linkToFollow.name} at ${href}`);
@@ -71,6 +81,18 @@ export class LinkService {
} }
} }
} }
return EMPTY;
}
/**
* Resolve the given {@link FollowLinkConfig} for the given model and return the model with the
* link property attached.
*
* @param model the {@link HALResource} to resolve the link for
* @param linkToFollow the {@link FollowLinkConfig} to resolve
*/
public resolveLink<T extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): T {
model[linkToFollow.name] = this.resolveLinkWithoutAttaching(model, linkToFollow);
return model; return model;
} }

View File

@@ -13,9 +13,14 @@ import { RestRequestMethod } from '../data/rest-request-method';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
import { ApplyPatchObjectCacheAction } from './object-cache.actions'; import { ApplyPatchObjectCacheAction } from './object-cache.actions';
import { ObjectCacheService } from './object-cache.service'; import { ObjectCacheService } from './object-cache.service';
import { CommitSSBAction, EmptySSBAction, ServerSyncBufferActionTypes } from './server-sync-buffer.actions'; import {
CommitSSBAction,
EmptySSBAction,
ServerSyncBufferActionTypes
} from './server-sync-buffer.actions';
import { ServerSyncBufferEffects } from './server-sync-buffer.effects'; import { ServerSyncBufferEffects } from './server-sync-buffer.effects';
import { storeModuleConfig } from '../../app.reducer'; import { storeModuleConfig } from '../../app.reducer';
import { NoOpAction } from '../../shared/ngrx/no-op.action';
describe('ServerSyncBufferEffects', () => { describe('ServerSyncBufferEffects', () => {
let ssbEffects: ServerSyncBufferEffects; let ssbEffects: ServerSyncBufferEffects;
@@ -143,7 +148,7 @@ describe('ServerSyncBufferEffects', () => {
payload: { method: RestRequestMethod.PATCH } payload: { method: RestRequestMethod.PATCH }
} }
}); });
const expected = cold('b', { b: { type: 'NO_ACTION' } }); const expected = cold('b', { b: new NoOpAction() });
expect(ssbEffects.commitServerSyncBuffer).toBeObservable(expected); expect(ssbEffects.commitServerSyncBuffer).toBeObservable(expected);
}); });

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