mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge remote-tracking branch 'upstream/main' into w2p-76654_PaginationService
This commit is contained in:
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
@@ -80,6 +80,19 @@ jobs:
|
||||
docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
|
||||
docker container ls
|
||||
|
||||
# Wait until the REST API returns a 200 response (or for a max of 30 seconds)
|
||||
# https://github.com/nev7n/wait_for_response
|
||||
- name: Wait for DSpace REST Backend to be ready (for e2e tests)
|
||||
uses: nev7n/wait_for_response@v1
|
||||
with:
|
||||
# We use the 'sites' endpoint to also ensure the database is ready
|
||||
url: 'http://localhost:8080/server/api/core/sites'
|
||||
responseCode: 200
|
||||
timeout: 30000
|
||||
|
||||
- name: Get DSpace REST Backend info/properties
|
||||
run: curl http://localhost:8080/server/api
|
||||
|
||||
- name: Run e2e tests (integration tests)
|
||||
run: yarn run e2e:ci
|
||||
|
||||
|
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -1,3 +1,7 @@
|
||||
{
|
||||
"typescript.check.workspaceVersion": false
|
||||
"typescript.check.workspaceVersion": false,
|
||||
"i18n-ally.localesPaths": [
|
||||
"src/assets/i18n",
|
||||
"src/app/core/locale"
|
||||
]
|
||||
}
|
@@ -14,22 +14,22 @@ export class ProtractorPage {
|
||||
}
|
||||
|
||||
getCurrentQuery(): promise.Promise<string> {
|
||||
return element(by.css('#search-navbar-container form input')).getAttribute('value');
|
||||
return element(by.css('.navbar-container #search-navbar-container form input')).getAttribute('value');
|
||||
}
|
||||
|
||||
expandAndFocusSearchBox() {
|
||||
element(by.css('#search-navbar-container form a')).click();
|
||||
element(by.css('.navbar-container #search-navbar-container form a')).click();
|
||||
}
|
||||
|
||||
setCurrentQuery(query: string) {
|
||||
element(by.css('#search-navbar-container form input[name="query"]')).sendKeys(query);
|
||||
element(by.css('.navbar-container #search-navbar-container form input[name="query"]')).sendKeys(query);
|
||||
}
|
||||
|
||||
submitNavbarSearchForm() {
|
||||
element(by.css('#search-navbar-container form .submit-icon')).click();
|
||||
element(by.css('.navbar-container #search-navbar-container form .submit-icon')).click();
|
||||
}
|
||||
|
||||
submitByPressingEnter() {
|
||||
element(by.css('#search-navbar-container form input[name="query"]')).sendKeys(protractor.Key.ENTER);
|
||||
element(by.css('.navbar-container #search-navbar-container form input[name="query"]')).sendKeys(protractor.Key.ENTER);
|
||||
}
|
||||
}
|
||||
|
@@ -119,10 +119,11 @@
|
||||
"rxjs": "^6.6.3",
|
||||
"rxjs-spy": "^7.5.3",
|
||||
"sass-resources-loader": "^2.1.1",
|
||||
"sortablejs": "1.10.1",
|
||||
"sortablejs": "1.13.0",
|
||||
"tslib": "^2.0.0",
|
||||
"webfontloader": "1.6.28",
|
||||
"zone.js": "^0.10.3"
|
||||
"zone.js": "^0.10.3",
|
||||
"@kolkov/ngx-gallery": "^1.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "10.0.1",
|
||||
|
@@ -10,7 +10,7 @@ const targetPath = './src/environments/environment.ts';
|
||||
const colors = require('colors');
|
||||
require('dotenv').config();
|
||||
const merge = require('deepmerge');
|
||||
|
||||
const mergeOptions = { arrayMerge: (destinationArray, sourceArray, options) => sourceArray };
|
||||
const environment = process.argv[2];
|
||||
let environmentFilePath;
|
||||
let production = false;
|
||||
@@ -45,10 +45,10 @@ const processEnv = {
|
||||
} as GlobalConfig;
|
||||
|
||||
import(environmentFilePath)
|
||||
.then((file) => generateEnvironmentFile(merge.all([commonEnv, file.environment, processEnv])))
|
||||
.then((file) => generateEnvironmentFile(merge.all([commonEnv, file.environment, processEnv], mergeOptions)))
|
||||
.catch(() => {
|
||||
console.log(colors.yellow.bold(`No specific environment file found for ` + environment));
|
||||
generateEnvironmentFile(merge(commonEnv, processEnv))
|
||||
generateEnvironmentFile(merge(commonEnv, processEnv, mergeOptions))
|
||||
});
|
||||
|
||||
function generateEnvironmentFile(file: GlobalConfig): void {
|
||||
@@ -65,7 +65,7 @@ function generateEnvironmentFile(file: GlobalConfig): void {
|
||||
}
|
||||
|
||||
// allow to override a few important options by environment variables
|
||||
function createServerConfig(host?: string, port?: string, nameSpace?: string, ssl?: string): ServerConfig {
|
||||
function createServerConfig(host?: string, port?: string, nameSpace?: string, ssl?: string): ServerConfig {
|
||||
const result = {} as any;
|
||||
if (hasValue(host)) {
|
||||
result.host = host;
|
||||
|
@@ -1,31 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
|
||||
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
|
||||
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
||||
import { GROUP_EDIT_PATH } from './admin-access-control-routing-paths';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{ path: 'epeople', component: EPeopleRegistryComponent, data: { title: 'admin.access-control.epeople.title' } },
|
||||
{ path: GROUP_EDIT_PATH, component: GroupsRegistryComponent, data: { title: 'admin.access-control.groups.title' } },
|
||||
{
|
||||
path: `${GROUP_EDIT_PATH}/:groupId`,
|
||||
component: GroupFormComponent,
|
||||
data: {title: 'admin.access-control.groups.title.singleGroup'}
|
||||
},
|
||||
{
|
||||
path: `${GROUP_EDIT_PATH}/newGroup`,
|
||||
component: GroupFormComponent,
|
||||
data: {title: 'admin.access-control.groups.title.addGroup'}
|
||||
},
|
||||
])
|
||||
]
|
||||
})
|
||||
/**
|
||||
* Routing module for the AccessControl section of the admin sidebar
|
||||
*/
|
||||
export class AdminAccessControlRoutingModule {
|
||||
|
||||
}
|
@@ -2,12 +2,7 @@ import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
import { getAdminModuleRoute } from '../app-routing-paths';
|
||||
|
||||
export const REGISTRIES_MODULE_PATH = 'registries';
|
||||
export const ACCESS_CONTROL_MODULE_PATH = 'access-control';
|
||||
|
||||
export function getRegistriesModuleRoute() {
|
||||
return new URLCombiner(getAdminModuleRoute(), REGISTRIES_MODULE_PATH).toString();
|
||||
}
|
||||
|
||||
export function getAccessControlModuleRoute() {
|
||||
return new URLCombiner(getAdminModuleRoute(), ACCESS_CONTROL_MODULE_PATH).toString();
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
|
||||
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
||||
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
||||
import { ACCESS_CONTROL_MODULE_PATH, REGISTRIES_MODULE_PATH } from './admin-routing-paths';
|
||||
import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -16,11 +16,6 @@ import { ACCESS_CONTROL_MODULE_PATH, REGISTRIES_MODULE_PATH } from './admin-rout
|
||||
loadChildren: () => import('./admin-registries/admin-registries.module')
|
||||
.then((m) => m.AdminRegistriesModule),
|
||||
},
|
||||
{
|
||||
path: ACCESS_CONTROL_MODULE_PATH,
|
||||
loadChildren: () => import('./admin-access-control/admin-access-control.module')
|
||||
.then((m) => m.AdminAccessControlModule),
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
|
@@ -61,7 +61,7 @@ export class ItemAdminSearchResultGridElementComponent extends SearchResultGridE
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the component depending on the item's relationship type, view mode and context
|
||||
* Fetch the component depending on the item's entity type, view mode and context
|
||||
* @returns {GenericConstructor<Component>}
|
||||
*/
|
||||
private getComponent(): GenericConstructor<Component> {
|
||||
|
@@ -11,6 +11,8 @@ import { Collection } from '../../../../../core/shared/collection.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
describe('CollectionAdminSearchResultListElementComponent', () => {
|
||||
let component: CollectionAdminSearchResultListElementComponent;
|
||||
@@ -33,7 +35,8 @@ describe('CollectionAdminSearchResultListElementComponent', () => {
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [CollectionAdminSearchResultListElementComponent],
|
||||
providers: [{ provide: TruncatableService, useValue: {} }],
|
||||
providers: [{ provide: TruncatableService, useValue: {} },
|
||||
{ provide: DSONameService, useClass: DSONameServiceMock }],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
|
@@ -11,6 +11,8 @@ import { CommunityAdminSearchResultListElementComponent } from './community-admi
|
||||
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
|
||||
import { Community } from '../../../../../core/shared/community.model';
|
||||
import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
describe('CommunityAdminSearchResultListElementComponent', () => {
|
||||
let component: CommunityAdminSearchResultListElementComponent;
|
||||
@@ -33,7 +35,8 @@ describe('CommunityAdminSearchResultListElementComponent', () => {
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [CommunityAdminSearchResultListElementComponent],
|
||||
providers: [{ provide: TruncatableService, useValue: {} }],
|
||||
providers: [{ provide: TruncatableService, useValue: {} },
|
||||
{ provide: DSONameService, useClass: DSONameServiceMock }],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
|
@@ -8,6 +8,8 @@ import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||
import { ItemAdminSearchResultListElementComponent } from './item-admin-search-result-list-element.component';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
describe('ItemAdminSearchResultListElementComponent', () => {
|
||||
let component: ItemAdminSearchResultListElementComponent;
|
||||
@@ -30,7 +32,8 @@ describe('ItemAdminSearchResultListElementComponent', () => {
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [ItemAdminSearchResultListElementComponent],
|
||||
providers: [{ provide: TruncatableService, useValue: {} }],
|
||||
providers: [{ provide: TruncatableService, useValue: {} },
|
||||
{ provide: DSONameService, useClass: DSONameServiceMock }],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
.compileComponents();
|
||||
|
@@ -1,27 +1,28 @@
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 edit-link" [routerLink]="[getEditRoute()]" [title]="'admin.search.item.edit' | translate">
|
||||
<i class="fa fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span>
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary my-1 move-link" [routerLink]="[getMoveRoute()]" [title]="'admin.search.item.move' | translate">
|
||||
<i class="fa fa-arrow-circle-right"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isWithdrawn" class="btn btn-light my-1 withdraw-link" [routerLink]="[getWithdrawRoute()]" [title]="'admin.search.item.withdraw' | translate">
|
||||
<i class="fa fa-ban"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isWithdrawn" class="btn btn-light my-1 reinstate-link" [routerLink]="[getReinstateRoute()]" [title]="'admin.search.item.reinstate' | translate">
|
||||
<i class="fa fa-undo"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.reinstate" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isDiscoverable" class="btn btn-light my-1 private-link" [routerLink]="[getPrivateRoute()]" [title]="'admin.search.item.make-private' | translate">
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isDiscoverable" class="btn btn-secondary my-1 private-link" [routerLink]="[getPrivateRoute()]" [title]="'admin.search.item.make-private' | translate">
|
||||
<i class="fa fa-eye-slash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-private" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isDiscoverable" class="btn btn-light my-1 public-link" [routerLink]="[getPublicRoute()]" [title]="'admin.search.item.make-public' | translate">
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isDiscoverable" class="btn btn-secondary my-1 public-link" [routerLink]="[getPublicRoute()]" [title]="'admin.search.item.make-public' | translate">
|
||||
<i class="fa fa-eye"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-public" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.search.item.delete' | translate">
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary my-1 edit-link" [routerLink]="[getEditRoute()]" [title]="'admin.search.item.edit' | translate">
|
||||
<i class="fa fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isWithdrawn" class="btn btn-warning t my-1 withdraw-link" [routerLink]="[getWithdrawRoute()]" [title]="'admin.search.item.withdraw' | translate">
|
||||
<i class="fa fa-ban"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isWithdrawn" class="btn btn-warning my-1 reinstate-link" [routerLink]="[getReinstateRoute()]" [title]="'admin.search.item.reinstate' | translate">
|
||||
<i class="fa fa-undo"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.reinstate" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-danger my-1 delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.search.item.delete' | translate">
|
||||
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.delete" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 move-link" [routerLink]="[getMoveRoute()]" [title]="'admin.search.item.move' | translate">
|
||||
<i class="fa fa-arrow-circle-right"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span>
|
||||
</a>
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<li class="sidebar-section">
|
||||
<a class="nav-item nav-link shortcut-icon" [routerLink]="itemModel.link">
|
||||
<i class="fas fa-{{section.icon}} fa-fw" [title]="('menu.section.icon.' + section.id) | translate"></i>
|
||||
<a class="nav-item nav-link shortcut-icon" attr.aria-labelledby="sidebarName-{{section.id}}" [title]="('menu.section.icon.' + section.id) | translate" [routerLink]="itemModel.link">
|
||||
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
||||
</a>
|
||||
<div class="sidebar-collapsible">
|
||||
<span class="section-header-text">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</span>
|
||||
<span id="sidebarName-{{section.id}}" class="section-header-text">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
@@ -1,11 +1,12 @@
|
||||
<nav @slideHorizontal class="navbar navbar-dark p-0"
|
||||
[ngClass]="{'active': sidebarOpen, 'inactive': sidebarClosed}"
|
||||
[@slideSidebar]="{
|
||||
value: (!(sidebarExpanded | async) ? 'collapsed' : 'expanded'),
|
||||
params: {sidebarWidth: (sidebarWidth | async)}
|
||||
}" (@slideSidebar.done)="finishSlide($event)" (@slideSidebar.start)="startSlide($event)"
|
||||
value: (!(sidebarExpanded | async) ? 'collapsed' : 'expanded'),
|
||||
params: {sidebarWidth: (sidebarWidth | async)}
|
||||
}" (@slideSidebar.done)="finishSlide($event)" (@slideSidebar.start)="startSlide($event)"
|
||||
*ngIf="menuVisible | async" (mouseenter)="expandPreview($event)"
|
||||
(mouseleave)="collapsePreview($event)">
|
||||
(mouseleave)="collapsePreview($event)"
|
||||
role="navigation" [attr.aria-label]="'menu.header.admin.description' |translate">
|
||||
<div class="sidebar-top-level-items">
|
||||
<ul class="navbar-nav">
|
||||
<li class="admin-menu-header sidebar-section">
|
||||
|
@@ -16,6 +16,8 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
import createSpy = jasmine.createSpy;
|
||||
|
||||
describe('AdminSidebarComponent', () => {
|
||||
let comp: AdminSidebarComponent;
|
||||
@@ -170,4 +172,150 @@ describe('AdminSidebarComponent', () => {
|
||||
expect(menuService.collapseMenuPreview).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('menu', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'addSection');
|
||||
});
|
||||
|
||||
describe('for regular user', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake(() => {
|
||||
return observableOf(false);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
comp.createMenu();
|
||||
});
|
||||
|
||||
it('should not show site admin section', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'admin_search', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'registries', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
parentID: 'registries', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'curation_tasks', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'workflow', visible: false,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not show edit_community', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'edit_community', visible: false,
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
it('should not show edit_collection', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'edit_collection', visible: false,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not show access control section', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'access_control', visible: false,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
parentID: 'access_control', visible: false,
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('for site admin', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
||||
return observableOf(featureID === FeatureID.AdministratorOf);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
comp.createMenu();
|
||||
});
|
||||
|
||||
it('should contain site admin section', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'admin_search', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'registries', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
parentID: 'registries', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'curation_tasks', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'workflow', visible: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('for community admin', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
||||
return observableOf(featureID === FeatureID.IsCommunityAdmin);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
comp.createMenu();
|
||||
});
|
||||
|
||||
it('should show edit_community', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'edit_community', visible: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('for collection admin', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
||||
return observableOf(featureID === FeatureID.IsCollectionAdmin);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
comp.createMenu();
|
||||
});
|
||||
|
||||
it('should show edit_collection', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'edit_collection', visible: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('for group admin', () => {
|
||||
beforeEach(() => {
|
||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
||||
return observableOf(featureID === FeatureID.CanManageGroups);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
comp.createMenu();
|
||||
});
|
||||
|
||||
it('should show access control section', () => {
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
id: 'access_control', visible: true,
|
||||
}));
|
||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
||||
parentID: 'access_control', visible: true,
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Component, Injector, OnInit } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { combineLatest, combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { first, map, take } from 'rxjs/operators';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||
@@ -76,9 +76,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.createMenu();
|
||||
this.createSiteAdministratorMenuSections();
|
||||
this.createExportMenuSections();
|
||||
this.createImportMenuSections();
|
||||
super.ngOnInit();
|
||||
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
|
||||
this.authService.isAuthenticated()
|
||||
@@ -102,192 +99,210 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
* Initialize all menu sections and items for this menu
|
||||
*/
|
||||
createMenu() {
|
||||
const menuList = [
|
||||
/* News */
|
||||
{
|
||||
id: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.new'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'plus-circle',
|
||||
index: 0
|
||||
},
|
||||
{
|
||||
id: 'new_community',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_community',
|
||||
function: () => {
|
||||
this.modalService.open(CreateCommunityParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_collection',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_collection',
|
||||
function: () => {
|
||||
this.modalService.open(CreateCollectionParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_item',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_item',
|
||||
function: () => {
|
||||
this.modalService.open(CreateItemParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_process',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.new_process',
|
||||
link: '/processes/new'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_item_version',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.new_item_version',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
this.createMainMenuSections();
|
||||
this.createSiteAdministratorMenuSections();
|
||||
this.createExportMenuSections();
|
||||
this.createImportMenuSections();
|
||||
this.createAccessControlMenuSections();
|
||||
}
|
||||
|
||||
/* Edit */
|
||||
{
|
||||
id: 'edit',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.edit'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'pencil-alt',
|
||||
index: 1
|
||||
},
|
||||
{
|
||||
id: 'edit_community',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_community',
|
||||
function: () => {
|
||||
this.modalService.open(EditCommunitySelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'edit_collection',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_collection',
|
||||
function: () => {
|
||||
this.modalService.open(EditCollectionSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'edit_item',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_item',
|
||||
function: () => {
|
||||
this.modalService.open(EditItemSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
/**
|
||||
* Initialize the main menu sections.
|
||||
* edit_community / edit_collection is only included if the current user is a Community or Collection admin
|
||||
*/
|
||||
createMainMenuSections() {
|
||||
combineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin),
|
||||
this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin),
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf)
|
||||
]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin]) => {
|
||||
const menuList = [
|
||||
/* News */
|
||||
{
|
||||
id: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.new'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'plus',
|
||||
index: 0
|
||||
},
|
||||
{
|
||||
id: 'new_community',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: isCommunityAdmin,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_community',
|
||||
function: () => {
|
||||
this.modalService.open(CreateCommunityParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_collection',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: isCommunityAdmin,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_collection',
|
||||
function: () => {
|
||||
this.modalService.open(CreateCollectionParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_item',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_item',
|
||||
function: () => {
|
||||
this.modalService.open(CreateItemParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_process',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: isCollectionAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.new_process',
|
||||
link: '/processes/new'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'new_item_version',
|
||||
parentID: 'new',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.new_item_version',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
|
||||
/* Curation tasks */
|
||||
{
|
||||
id: 'curation_tasks',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.curation_task',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'filter',
|
||||
index: 7
|
||||
},
|
||||
/* Edit */
|
||||
{
|
||||
id: 'edit',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.edit'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'pencil-alt',
|
||||
index: 1
|
||||
},
|
||||
{
|
||||
id: 'edit_community',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: isCommunityAdmin,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_community',
|
||||
function: () => {
|
||||
this.modalService.open(EditCommunitySelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'edit_collection',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: isCollectionAdmin,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_collection',
|
||||
function: () => {
|
||||
this.modalService.open(EditCollectionSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'edit_item',
|
||||
parentID: 'edit',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_item',
|
||||
function: () => {
|
||||
this.modalService.open(EditItemSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
|
||||
/* Statistics */
|
||||
{
|
||||
id: 'statistics_task',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.statistics_task',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'chart-bar',
|
||||
index: 8
|
||||
},
|
||||
/* Curation tasks */
|
||||
{
|
||||
id: 'curation_tasks',
|
||||
active: false,
|
||||
visible: isCollectionAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.curation_task',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'filter',
|
||||
index: 7
|
||||
},
|
||||
|
||||
/* Control Panel */
|
||||
{
|
||||
id: 'control_panel',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.control_panel',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'cogs',
|
||||
index: 9
|
||||
},
|
||||
/* Statistics */
|
||||
{
|
||||
id: 'statistics_task',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.statistics_task',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'chart-bar',
|
||||
index: 8
|
||||
},
|
||||
|
||||
/* Processes */
|
||||
{
|
||||
id: 'processes',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.processes',
|
||||
link: '/processes'
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'terminal',
|
||||
index: 10
|
||||
},
|
||||
];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
||||
shouldPersistOnRouteChange: true
|
||||
})));
|
||||
/* Control Panel */
|
||||
{
|
||||
id: 'control_panel',
|
||||
active: false,
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.control_panel',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'cogs',
|
||||
index: 9
|
||||
},
|
||||
|
||||
/* Processes */
|
||||
{
|
||||
id: 'processes',
|
||||
active: false,
|
||||
visible: isSiteAdmin,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.processes',
|
||||
link: '/processes'
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'terminal',
|
||||
index: 10
|
||||
},
|
||||
];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
||||
shouldPersistOnRouteChange: true
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,7 +320,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.export'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'sign-out-alt',
|
||||
icon: 'file-export',
|
||||
index: 3,
|
||||
shouldPersistOnRouteChange: true
|
||||
},
|
||||
@@ -388,7 +403,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.import'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'sign-in-alt',
|
||||
icon: 'file-import',
|
||||
index: 2
|
||||
},
|
||||
{
|
||||
@@ -436,51 +451,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
createSiteAdministratorMenuSections() {
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf).subscribe((authorized) => {
|
||||
const menuList = [
|
||||
/* Access Control */
|
||||
{
|
||||
id: 'access_control',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.access_control'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'key',
|
||||
index: 4
|
||||
},
|
||||
{
|
||||
id: 'access_control_people',
|
||||
parentID: 'access_control',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_people',
|
||||
link: '/admin/access-control/epeople'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'access_control_groups',
|
||||
parentID: 'access_control',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_groups',
|
||||
link: '/admin/access-control/groups'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'access_control_authorizations',
|
||||
parentID: 'access_control',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_authorizations',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
/* Admin Search */
|
||||
{
|
||||
id: 'admin_search',
|
||||
@@ -564,6 +534,65 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create menu sections dependent on whether or not the current user can manage access control groups
|
||||
*/
|
||||
createAccessControlMenuSections() {
|
||||
this.authorizationService.isAuthorized(FeatureID.CanManageGroups).subscribe((authorized) => {
|
||||
const menuList = [
|
||||
/* Access Control */
|
||||
{
|
||||
id: 'access_control_people',
|
||||
parentID: 'access_control',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_people',
|
||||
link: '/access-control/epeople'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'access_control_groups',
|
||||
parentID: 'access_control',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_groups',
|
||||
link: '/access-control/groups'
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'access_control_authorizations',
|
||||
parentID: 'access_control',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.access_control_authorizations',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
},
|
||||
{
|
||||
id: 'access_control',
|
||||
active: false,
|
||||
visible: authorized,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.access_control'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'key',
|
||||
index: 4
|
||||
},
|
||||
];
|
||||
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
||||
shouldPersistOnRouteChange: true,
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to change this.collapsed to false when the slide animation ends and is sliding open
|
||||
* @param event The animation event
|
||||
|
@@ -3,14 +3,14 @@
|
||||
value: ((expanded | async) ? 'endBackground' : 'startBackground'),
|
||||
params: {endColor: (sidebarActiveBg | async)}}">
|
||||
<div class="icon-wrapper">
|
||||
<a class="nav-item nav-link shortcut-icon" (click)="toggleSection($event)" href="#">
|
||||
<i class="fas fa-{{section.icon}} fa-fw" [title]="('menu.section.icon.' + section.id) | translate"></i>
|
||||
<a class="nav-item nav-link shortcut-icon" attr.aria.labelledby="sidebarName-{{section.id}}" [title]="('menu.section.icon.' + section.id) | translate" (click)="toggleSection($event)" href="#">
|
||||
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-collapsible">
|
||||
<a class="nav-item nav-link" href="#"
|
||||
(click)="toggleSection($event)">
|
||||
<span class="section-header-text">
|
||||
<span id="sidebarName-{{section.id}}" class="section-header-text">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</span>
|
||||
|
@@ -16,4 +16,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
li.sidebar-section.expanded {
|
||||
background-color: var(--ds-admin-sidebar-active-bg) !important;
|
||||
}
|
||||
}
|
||||
|
@@ -96,7 +96,7 @@ export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends S
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the component depending on the item's relationship type, view mode and context
|
||||
* Fetch the component depending on the item's entity type, view mode and context
|
||||
* @returns {GenericConstructor<Component>}
|
||||
*/
|
||||
private getComponent(item: Item): GenericConstructor<Component> {
|
||||
|
@@ -16,6 +16,8 @@ import { Item } from '../../../../../core/shared/item.model';
|
||||
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||
|
||||
describe('WorkflowItemAdminWorkflowListElementComponent', () => {
|
||||
let component: WorkflowItemSearchResultAdminWorkflowListElementComponent;
|
||||
@@ -49,6 +51,7 @@ describe('WorkflowItemAdminWorkflowListElementComponent', () => {
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||
{ provide: LinkService, useValue: linkService },
|
||||
{ provide: DSONameService, useClass: DSONameServiceMock }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
|
@@ -12,6 +12,7 @@ import { Item } from '../../../../../core/shared/item.model';
|
||||
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@listableObjectComponent(WorkflowItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch)
|
||||
@Component({
|
||||
@@ -29,8 +30,11 @@ export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends S
|
||||
*/
|
||||
public item$: Observable<Item>;
|
||||
|
||||
constructor(private linkService: LinkService, protected truncatableService: TruncatableService) {
|
||||
super(truncatableService);
|
||||
constructor(private linkService: LinkService,
|
||||
protected truncatableService: TruncatableService,
|
||||
protected dsoNameService: DSONameService
|
||||
) {
|
||||
super(truncatableService, dsoNameService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { AdminAccessControlModule } from './admin-access-control/admin-access-control.module';
|
||||
import { AccessControlModule } from '../access-control/access-control.module';
|
||||
import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component';
|
||||
import { AdminRegistriesModule } from './admin-registries/admin-registries.module';
|
||||
import { AdminRoutingModule } from './admin-routing.module';
|
||||
@@ -21,7 +21,7 @@ const ENTRY_COMPONENTS = [
|
||||
imports: [
|
||||
AdminRoutingModule,
|
||||
AdminRegistriesModule,
|
||||
AdminAccessControlModule,
|
||||
AccessControlModule,
|
||||
AdminSearchModule.withEntryComponents(),
|
||||
AdminWorkflowModuleModule.withEntryComponents(),
|
||||
SharedModule,
|
||||
|
@@ -0,0 +1,28 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { BrowseBySwitcherComponent } from './browse-by-switcher.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for BrowseBySwitcherComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-browse-by-switcher',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../shared/theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedBrowseBySwitcherComponent extends ThemedComponent<BrowseBySwitcherComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'BrowseBySwitcherComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/+browse-by/+browse-by-switcher/browse-by-switcher.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./browse-by-switcher.component`);
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowseByGuard } from './browse-by-guard';
|
||||
import { BrowseBySwitcherComponent } from './+browse-by-switcher/browse-by-switcher.component';
|
||||
import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver';
|
||||
import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver';
|
||||
import { ThemedBrowseBySwitcherComponent } from './+browse-by-switcher/themed-browse-by-switcher.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -14,7 +14,7 @@ import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.reso
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
component: BrowseBySwitcherComponent,
|
||||
component: ThemedBrowseBySwitcherComponent,
|
||||
canActivate: [BrowseByGuard],
|
||||
resolve: { breadcrumb: BrowseByI18nBreadcrumbResolver },
|
||||
data: { title: 'browse.title', breadcrumbKey: 'browse.metadata' }
|
||||
|
@@ -5,6 +5,7 @@ import { SharedModule } from '../shared/shared.module';
|
||||
import { BrowseByMetadataPageComponent } from './+browse-by-metadata-page/browse-by-metadata-page.component';
|
||||
import { BrowseByDatePageComponent } from './+browse-by-date-page/browse-by-date-page.component';
|
||||
import { BrowseBySwitcherComponent } from './+browse-by-switcher/browse-by-switcher.component';
|
||||
import { ThemedBrowseBySwitcherComponent } from './+browse-by-switcher/themed-browse-by-switcher.component';
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put only entry components that use custom decorator
|
||||
@@ -20,6 +21,7 @@ const ENTRY_COMPONENTS = [
|
||||
],
|
||||
declarations: [
|
||||
BrowseBySwitcherComponent,
|
||||
ThemedBrowseBySwitcherComponent,
|
||||
...ENTRY_COMPONENTS
|
||||
],
|
||||
exports: [
|
||||
|
@@ -7,7 +7,6 @@ import {
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
|
||||
import { Location } from '@angular/common';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
@@ -76,14 +75,13 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> {
|
||||
}),
|
||||
];
|
||||
|
||||
public constructor(protected location: Location,
|
||||
protected formService: DynamicFormService,
|
||||
public constructor(protected formService: DynamicFormService,
|
||||
protected translate: TranslateService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected authService: AuthService,
|
||||
protected dsoService: CommunityDataService,
|
||||
protected requestService: RequestService,
|
||||
protected objectCache: ObjectCacheService) {
|
||||
super(location, formService, translate, notificationsService, authService, requestService, objectCache);
|
||||
super(formService, translate, notificationsService, authService, requestService, objectCache);
|
||||
}
|
||||
}
|
||||
|
@@ -31,6 +31,7 @@
|
||||
[scope]="(searchOptions$ | async)?.scope"
|
||||
[currentUrl]="'./'"
|
||||
[inPlaceSearch]="true"
|
||||
[searchPlaceholder]="'collection.edit.item-mapper.search-form.placeholder' | translate"
|
||||
(submitSearch)="performedSearch = true">
|
||||
</ds-search-form>
|
||||
</div>
|
||||
|
@@ -24,6 +24,10 @@ export function getCollectionEditRolesRoute(id) {
|
||||
return new URLCombiner(getCollectionPageRoute(id), COLLECTION_EDIT_PATH, COLLECTION_EDIT_ROLES_PATH).toString();
|
||||
}
|
||||
|
||||
export function getCollectionItemTemplateRoute(id) {
|
||||
return new URLCombiner(getCollectionPageRoute(id), ITEMTEMPLATE_PATH).toString();
|
||||
}
|
||||
|
||||
export const COLLECTION_CREATE_PATH = 'create';
|
||||
export const COLLECTION_EDIT_PATH = 'edit';
|
||||
export const COLLECTION_EDIT_ROLES_PATH = 'roles';
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { CollectionPageComponent } from './collection-page.component';
|
||||
import { CollectionPageResolver } from './collection-page.resolver';
|
||||
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
import { CollectionPageAdministratorGuard } from './collection-page-administrator.guard';
|
||||
import { MenuItemType } from '../shared/menu/initial-menus-state';
|
||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -62,7 +62,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: CollectionPageComponent,
|
||||
component: ThemedCollectionPageComponent,
|
||||
pathMatch: 'full',
|
||||
}
|
||||
],
|
||||
|
@@ -35,7 +35,7 @@
|
||||
</ds-comcol-page-content>
|
||||
</header>
|
||||
<div class="pl-2">
|
||||
<ds-dso-page-edit-button [pageRoute]="collectionPageRoute$ | async" [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>
|
||||
<section class="comcol-page-browse-section">
|
||||
|
@@ -27,6 +27,8 @@ import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
import { PaginationService } from '../core/pagination/pagination.service';
|
||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||
import { getCollectionPageRoute } from './collection-page-routing-paths';
|
||||
|
||||
@Component({
|
||||
@@ -50,6 +52,11 @@ export class CollectionPageComponent implements OnInit {
|
||||
sortConfig: SortOptions
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Whether the current user is a Community admin
|
||||
*/
|
||||
isCollectionAdmin$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Route to the community page
|
||||
*/
|
||||
@@ -63,7 +70,7 @@ export class CollectionPageComponent implements OnInit {
|
||||
private router: Router,
|
||||
private authService: AuthService,
|
||||
private paginationService: PaginationService,
|
||||
|
||||
private authorizationDataService: AuthorizationDataService,
|
||||
) {
|
||||
this.paginationConfig = new PaginationComponentOptions();
|
||||
this.paginationConfig.id = 'cp';
|
||||
@@ -83,6 +90,7 @@ export class CollectionPageComponent implements OnInit {
|
||||
filter((collection: Collection) => hasValue(collection)),
|
||||
mergeMap((collection: Collection) => collection.logo)
|
||||
);
|
||||
this.isCollectionAdmin$ = this.authorizationDataService.isAuthorized(FeatureID.IsCollectionAdmin);
|
||||
|
||||
this.paginationChanges$ = new BehaviorSubject({
|
||||
paginationConfig: this.paginationConfig,
|
||||
|
@@ -13,6 +13,7 @@ import { CollectionItemMapperComponent } from './collection-item-mapper/collecti
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
import { CollectionFormModule } from './collection-form/collection-form.module';
|
||||
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -25,6 +26,7 @@ import { CollectionFormModule } from './collection-form/collection-form.module';
|
||||
],
|
||||
declarations: [
|
||||
CollectionPageComponent,
|
||||
ThemedCollectionPageComponent,
|
||||
CreateCollectionPageComponent,
|
||||
DeleteCollectionPageComponent,
|
||||
EditItemTemplatePageComponent,
|
||||
|
@@ -4,5 +4,7 @@
|
||||
<h2 id="sub-header" class="border-bottom pb-2">{{'collection.create.sub-head' | translate:{ parent: (parentRD$| async)?.payload.name } }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<ds-collection-form (submitForm)="onSubmit($event)" (finish)="navigateToNewPage()"></ds-collection-form>
|
||||
<ds-collection-form (submitForm)="onSubmit($event)"
|
||||
(back)="navigateToHome()"
|
||||
(finish)="navigateToNewPage()"></ds-collection-form>
|
||||
</div>
|
||||
|
@@ -2,15 +2,18 @@
|
||||
<div class="row">
|
||||
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate
|
||||
}}</h2>
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate}}</h2>
|
||||
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||
<button class="btn btn-primary mr-2" (click)="onConfirm(dso)">
|
||||
{{'community.delete.confirm' |
|
||||
translate}}
|
||||
</button>
|
||||
<button class="btn btn-primary" (click)="onCancel(dso)">{{'community.delete.cancel' | translate}}
|
||||
</button>
|
||||
<div class="form-group row">
|
||||
<div class="col text-right">
|
||||
<button class="btn btn-outline-secondary" (click)="onCancel(dso)">
|
||||
<i class="fas fa-times"></i> {{'community.delete.cancel' | translate}}
|
||||
</button>
|
||||
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)">
|
||||
<i class="fas fa-trash"></i> {{'community.delete.confirm' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
@@ -16,9 +16,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ds-collection-form (submitForm)="onSubmit($event)"
|
||||
[dso]="(dsoRD$ | async)?.payload"
|
||||
<ds-collection-form [dso]="(dsoRD$ | async)?.payload"
|
||||
(submitForm)="onSubmit($event)"
|
||||
(back)="navigateToHomePage()"
|
||||
(finish)="navigateToHomePage()"></ds-collection-form>
|
||||
<a class="btn btn-danger"
|
||||
[routerLink]="'/collections/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'collection.edit.delete'
|
||||
| translate}}</a>
|
||||
|
@@ -15,6 +15,7 @@ import { Collection } from '../../../core/shared/collection.model';
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths';
|
||||
|
||||
describe('CollectionMetadataComponent', () => {
|
||||
let comp: CollectionMetadataComponent;
|
||||
@@ -35,11 +36,13 @@ describe('CollectionMetadataComponent', () => {
|
||||
self: { href: 'collection-selflink' }
|
||||
}
|
||||
});
|
||||
const collectionTemplateHref = 'rest/api/test/collections/template';
|
||||
|
||||
const itemTemplateServiceStub = Object.assign({
|
||||
findByCollectionID: () => createSuccessfulRemoteDataObject$(template),
|
||||
create: () => createSuccessfulRemoteDataObject$(template),
|
||||
deleteByCollectionID: () => observableOf(true)
|
||||
const itemTemplateServiceStub = jasmine.createSpyObj('itemTemplateService', {
|
||||
findByCollectionID: createSuccessfulRemoteDataObject$(template),
|
||||
create: createSuccessfulRemoteDataObject$(template),
|
||||
deleteByCollectionID: observableOf(true),
|
||||
getCollectionEndpoint: observableOf(collectionTemplateHref),
|
||||
});
|
||||
|
||||
const notificationsService = jasmine.createSpyObj('notificationsService', {
|
||||
@@ -50,7 +53,7 @@ describe('CollectionMetadataComponent', () => {
|
||||
remove: {}
|
||||
});
|
||||
const requestService = jasmine.createSpyObj('requestService', {
|
||||
removeByHrefSubstring: {}
|
||||
setStaleByHrefSubstring: {}
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
@@ -87,14 +90,14 @@ describe('CollectionMetadataComponent', () => {
|
||||
it('should navigate to the collection\'s itemtemplate page', () => {
|
||||
spyOn(router, 'navigate');
|
||||
comp.addItemTemplate();
|
||||
expect(router.navigate).toHaveBeenCalledWith(['collections', collection.uuid, 'itemtemplate']);
|
||||
expect(router.navigate).toHaveBeenCalledWith([getCollectionItemTemplateRoute(collection.uuid)]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteItemTemplate', () => {
|
||||
describe('when delete returns a success', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(itemTemplateService, 'deleteByCollectionID').and.returnValue(observableOf(true));
|
||||
(itemTemplateService.deleteByCollectionID as jasmine.Spy).and.returnValue(observableOf(true));
|
||||
comp.deleteItemTemplate();
|
||||
});
|
||||
|
||||
@@ -103,14 +106,15 @@ describe('CollectionMetadataComponent', () => {
|
||||
});
|
||||
|
||||
it('should reset related object and request cache', () => {
|
||||
expect(objectCache.remove).toHaveBeenCalledWith(template.self);
|
||||
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(collection.self);
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(collectionTemplateHref);
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(template.self);
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(collection.self);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when delete returns a failure', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(itemTemplateService, 'deleteByCollectionID').and.returnValue(observableOf(false));
|
||||
(itemTemplateService.deleteByCollectionID as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
comp.deleteItemTemplate();
|
||||
});
|
||||
|
||||
|
@@ -7,12 +7,13 @@ import { ItemTemplateDataService } from '../../../core/data/item-template-data.s
|
||||
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../core/shared/operators';
|
||||
import { switchMap, take } from 'rxjs/operators';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||
import { switchMap, tap } from 'rxjs/operators';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths';
|
||||
|
||||
/**
|
||||
* Component for editing a collection's metadata
|
||||
@@ -53,8 +54,7 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
|
||||
*/
|
||||
initTemplateItem() {
|
||||
this.itemTemplateRD$ = this.dsoRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid))
|
||||
);
|
||||
}
|
||||
@@ -64,19 +64,20 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
|
||||
*/
|
||||
addItemTemplate() {
|
||||
const collection$ = this.dsoRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
take(1)
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
);
|
||||
const template$ = collection$.pipe(
|
||||
switchMap((collection: Collection) => this.itemTemplateService.create(new Item(), collection.uuid)),
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
take(1)
|
||||
switchMap((collection: Collection) => this.itemTemplateService.create(new Item(), collection.uuid).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
)),
|
||||
);
|
||||
const templateHref$ = collection$.pipe(
|
||||
switchMap((collection) => this.itemTemplateService.getCollectionEndpoint(collection.id)),
|
||||
);
|
||||
|
||||
combineLatestObservable(collection$, template$).subscribe(([collection, template]) => {
|
||||
this.router.navigate(['collections', collection.uuid, 'itemtemplate']);
|
||||
combineLatestObservable(collection$, template$, templateHref$).subscribe(([collection, template, templateHref]) => {
|
||||
this.requestService.setStaleByHrefSubstring(templateHref);
|
||||
this.router.navigate([getCollectionItemTemplateRoute(collection.uuid)]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,23 +86,30 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
|
||||
*/
|
||||
deleteItemTemplate() {
|
||||
const collection$ = this.dsoRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
take(1)
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
);
|
||||
const template$ = collection$.pipe(
|
||||
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid)),
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
take(1)
|
||||
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
)),
|
||||
);
|
||||
const templateHref$ = collection$.pipe(
|
||||
switchMap((collection) => this.itemTemplateService.getCollectionEndpoint(collection.id)),
|
||||
);
|
||||
|
||||
combineLatestObservable(collection$, template$).pipe(
|
||||
switchMap(([collection, template]) => {
|
||||
const success$ = this.itemTemplateService.deleteByCollectionID(template, collection.uuid);
|
||||
this.objectCache.remove(template.self);
|
||||
this.requestService.removeByHrefSubstring(collection.self);
|
||||
return success$;
|
||||
combineLatestObservable(collection$, template$, templateHref$).pipe(
|
||||
switchMap(([collection, template, templateHref]) => {
|
||||
return this.itemTemplateService.deleteByCollectionID(template, collection.uuid).pipe(
|
||||
tap((success: boolean) => {
|
||||
if (success) {
|
||||
this.objectCache.remove(templateHref);
|
||||
this.objectCache.remove(template.self);
|
||||
this.requestService.setStaleByHrefSubstring(template.self);
|
||||
this.requestService.setStaleByHrefSubstring(templateHref);
|
||||
this.requestService.setStaleByHrefSubstring(collection.self);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
).subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
|
@@ -11,6 +11,7 @@ import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('CollectionRolesComponent', () => {
|
||||
|
||||
@@ -67,6 +68,7 @@ describe('CollectionRolesComponent', () => {
|
||||
SharedModule,
|
||||
RouterTestingModule.withRoutes([]),
|
||||
TranslateModule.forRoot(),
|
||||
NoopAnimationsModule
|
||||
],
|
||||
declarations: [
|
||||
CollectionRolesComponent,
|
||||
|
@@ -31,6 +31,7 @@
|
||||
[formModel]="formModel"
|
||||
[formLayout]="formLayout"
|
||||
[displaySubmit]="false"
|
||||
[displayCancel]="false"
|
||||
(dfChange)="onChange($event)"
|
||||
(submitForm)="onSubmit()"
|
||||
(cancel)="onCancel()"></ds-form>
|
||||
|
@@ -18,7 +18,14 @@ describe('EditCollectionPageComponent', () => {
|
||||
dso: { payload: {} }
|
||||
}),
|
||||
routeConfig: {
|
||||
children: []
|
||||
children: [
|
||||
{
|
||||
path: 'mockUrl',
|
||||
data: {
|
||||
hideReturnButton: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
snapshot: {
|
||||
firstChild: {
|
||||
|
@@ -12,6 +12,7 @@ import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/res
|
||||
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
|
||||
import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver';
|
||||
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
|
||||
import { CollectionAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard';
|
||||
|
||||
/**
|
||||
* Routing module that handles the routing for the Edit Collection page administrator functionality
|
||||
@@ -26,6 +27,7 @@ import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit
|
||||
},
|
||||
data: { breadcrumbKey: 'collection.edit' },
|
||||
component: EditCollectionPageComponent,
|
||||
canActivate: [CollectionAdministratorGuard],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
@@ -91,7 +93,7 @@ import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit
|
||||
{
|
||||
path: 'mapper',
|
||||
component: CollectionItemMapperComponent,
|
||||
data: { title: 'collection.edit.tabs.item-mapper.title', showBreadcrumbs: true }
|
||||
data: { title: 'collection.edit.tabs.item-mapper.title', hideReturnButton: true, showBreadcrumbs: true }
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@@ -1,9 +1,13 @@
|
||||
<div class="container" *ngVar="(collectionRD$ | async)?.payload as collection">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}</h2>
|
||||
<ds-item-metadata [updateService]="itemTemplateService"></ds-item-metadata>
|
||||
<button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
|
||||
<div class="col-12" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<ng-container *ngIf="itemRD?.hasSucceeded">
|
||||
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}</h2>
|
||||
<ds-item-metadata [updateService]="itemTemplateService" [item]="itemRD?.payload"></ds-item-metadata>
|
||||
<button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
|
||||
</ng-container>
|
||||
<ds-loading *ngIf="itemRD?.isLoading" [message]="'collection.edit.template.loading' | translate"></ds-loading>
|
||||
<ds-alert *ngIf="itemRD?.hasFailed" [type]="AlertTypeEnum.Error" [content]="'collection.edit.template.error' | translate"></ds-alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -9,7 +9,7 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { getCollectionEditRoute } from '../collection-page-routing-paths';
|
||||
|
||||
describe('EditItemTemplatePageComponent', () => {
|
||||
@@ -24,11 +24,14 @@ describe('EditItemTemplatePageComponent', () => {
|
||||
id: 'collection-id',
|
||||
name: 'Fake Collection'
|
||||
});
|
||||
itemTemplateService = jasmine.createSpyObj('itemTemplateService', {
|
||||
findByCollectionID: createSuccessfulRemoteDataObject$({})
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [EditItemTemplatePageComponent],
|
||||
providers: [
|
||||
{ provide: ItemTemplateDataService, useValue: {} },
|
||||
{ provide: ItemTemplateDataService, useValue: itemTemplateService },
|
||||
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -38,7 +41,6 @@ describe('EditItemTemplatePageComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EditItemTemplatePageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
itemTemplateService = (comp as any).itemTemplateService;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
|
@@ -3,9 +3,12 @@ import { Observable } from 'rxjs';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
import { first, map, switchMap } from 'rxjs/operators';
|
||||
import { ItemTemplateDataService } from '../../core/data/item-template-data.service';
|
||||
import { getCollectionEditRoute } from '../collection-page-routing-paths';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||
import { AlertType } from '../../shared/alert/aletr-type';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-item-template-page',
|
||||
@@ -21,12 +24,27 @@ export class EditItemTemplatePageComponent implements OnInit {
|
||||
*/
|
||||
collectionRD$: Observable<RemoteData<Collection>>;
|
||||
|
||||
/**
|
||||
* The template item
|
||||
*/
|
||||
itemRD$: Observable<RemoteData<Item>>;
|
||||
|
||||
/**
|
||||
* The AlertType enumeration
|
||||
* @type {AlertType}
|
||||
*/
|
||||
AlertTypeEnum = AlertType;
|
||||
|
||||
constructor(protected route: ActivatedRoute,
|
||||
public itemTemplateService: ItemTemplateDataService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.collectionRD$ = this.route.parent.data.pipe(first(), map((data) => data.dso));
|
||||
this.itemRD$ = this.collectionRD$.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((collection) => this.itemTemplateService.findByCollectionID(collection.id)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
26
src/app/+collection-page/themed-collection-page.component.ts
Normal file
26
src/app/+collection-page/themed-collection-page.component.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||
import { CollectionPageComponent } from './collection-page.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for CollectionPageComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-community-page',
|
||||
styleUrls: [],
|
||||
templateUrl: '../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedCollectionPageComponent extends ThemedComponent<CollectionPageComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'CollectionPageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../themes/${themeName}/app/+collection-page/collection-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./collection-page.component`);
|
||||
}
|
||||
|
||||
}
|
@@ -7,7 +7,6 @@ import {
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
|
||||
import { Location } from '@angular/common';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
@@ -68,14 +67,13 @@ export class CommunityFormComponent extends ComColFormComponent<Community> {
|
||||
}),
|
||||
];
|
||||
|
||||
public constructor(protected location: Location,
|
||||
protected formService: DynamicFormService,
|
||||
public constructor(protected formService: DynamicFormService,
|
||||
protected translate: TranslateService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected authService: AuthService,
|
||||
protected dsoService: CommunityDataService,
|
||||
protected requestService: RequestService,
|
||||
protected objectCache: ObjectCacheService) {
|
||||
super(location, formService, translate, notificationsService, authService, requestService, objectCache);
|
||||
super(formService, translate, notificationsService, authService, requestService, objectCache);
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { CommunityPageComponent } from './community-page.component';
|
||||
import { CommunityPageResolver } from './community-page.resolver';
|
||||
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
@@ -14,6 +13,7 @@ import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-rou
|
||||
import { CommunityPageAdministratorGuard } from './community-page-administrator.guard';
|
||||
import { MenuItemType } from '../shared/menu/initial-menus-state';
|
||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
import { ThemedCommunityPageComponent } from './themed-community-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -45,7 +45,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: CommunityPageComponent,
|
||||
component: ThemedCommunityPageComponent,
|
||||
pathMatch: 'full',
|
||||
}
|
||||
],
|
||||
|
@@ -21,7 +21,7 @@
|
||||
</ds-comcol-page-content>
|
||||
</header>
|
||||
<div class="pl-2">
|
||||
<ds-dso-page-edit-button [pageRoute]="communityPageRoute$ | async" [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>
|
||||
<section class="comcol-page-browse-section">
|
||||
|
@@ -15,6 +15,8 @@ import { fadeInOut } from '../shared/animations/fade';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../core/shared/operators';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||
import { getCommunityPageRoute } from './community-page-routing-paths';
|
||||
|
||||
@Component({
|
||||
@@ -33,6 +35,11 @@ export class CommunityPageComponent implements OnInit {
|
||||
*/
|
||||
communityRD$: Observable<RemoteData<Community>>;
|
||||
|
||||
/**
|
||||
* Whether the current user is a Community admin
|
||||
*/
|
||||
isCommunityAdmin$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The logo of this community
|
||||
*/
|
||||
@@ -49,6 +56,7 @@ export class CommunityPageComponent implements OnInit {
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private authService: AuthService,
|
||||
private authorizationDataService: AuthorizationDataService
|
||||
) {
|
||||
|
||||
}
|
||||
@@ -66,6 +74,6 @@ export class CommunityPageComponent implements OnInit {
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
map((community) => getCommunityPageRoute(community.id))
|
||||
);
|
||||
this.isCommunityAdmin$ = this.authorizationDataService.isAuthorized(FeatureID.IsCommunityAdmin);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -11,6 +11,14 @@ import { CreateCommunityPageComponent } from './create-community-page/create-com
|
||||
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
import { CommunityFormModule } from './community-form/community-form.module';
|
||||
import { ThemedCommunityPageComponent } from './themed-community-page.component';
|
||||
|
||||
const DECLARATIONS = [CommunityPageComponent,
|
||||
ThemedCommunityPageComponent,
|
||||
CommunityPageSubCollectionListComponent,
|
||||
CommunityPageSubCommunityListComponent,
|
||||
CreateCommunityPageComponent,
|
||||
DeleteCommunityPageComponent];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -21,11 +29,10 @@ import { CommunityFormModule } from './community-form/community-form.module';
|
||||
CommunityFormModule
|
||||
],
|
||||
declarations: [
|
||||
CommunityPageComponent,
|
||||
CommunityPageSubCollectionListComponent,
|
||||
CommunityPageSubCommunityListComponent,
|
||||
CreateCommunityPageComponent,
|
||||
DeleteCommunityPageComponent
|
||||
...DECLARATIONS
|
||||
],
|
||||
exports: [
|
||||
...DECLARATIONS
|
||||
]
|
||||
})
|
||||
|
||||
|
@@ -7,5 +7,7 @@
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<ds-community-form (submitForm)="onSubmit($event)" (finish)="navigateToNewPage()"></ds-community-form>
|
||||
<ds-community-form (submitForm)="onSubmit($event)"
|
||||
(back)="navigateToHome()"
|
||||
(finish)="navigateToNewPage()"></ds-community-form>
|
||||
</div>
|
||||
|
@@ -2,18 +2,20 @@
|
||||
<div class="row">
|
||||
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate
|
||||
}}</h2>
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate}}</h2>
|
||||
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||
<button class="btn btn-primary mr-2" (click)="onConfirm(dso)">
|
||||
{{'community.delete.confirm' |
|
||||
translate}}
|
||||
</button>
|
||||
<button class="btn btn-primary" (click)="onCancel(dso)">{{'community.delete.cancel' | translate}}
|
||||
</button>
|
||||
<div class="form-group row">
|
||||
<div class="col text-right">
|
||||
<button class="btn btn-outline-secondary" (click)="onCancel(dso)">
|
||||
<i class="fas fa-times"></i> {{'community.delete.cancel' | translate}}
|
||||
</button>
|
||||
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)">
|
||||
<i class="fas fa-trash"></i> {{'community.delete.confirm' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@@ -1,6 +1,5 @@
|
||||
<ds-community-form (submitForm)="onSubmit($event)"
|
||||
[dso]="(dsoRD$ | async)?.payload"
|
||||
<ds-community-form [dso]="(dsoRD$ | async)?.payload"
|
||||
(submitForm)="onSubmit($event)"
|
||||
(back)="navigateToHomePage()"
|
||||
(finish)="navigateToHomePage()"></ds-community-form>
|
||||
<a class="btn btn-danger"
|
||||
[routerLink]="'/communities/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'community.edit.delete'
|
||||
| translate}}</a>
|
||||
|
||||
|
@@ -11,6 +11,7 @@ import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||
import { SharedModule } from '../../../shared/shared.module';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('CommunityRolesComponent', () => {
|
||||
|
||||
@@ -52,6 +53,7 @@ describe('CommunityRolesComponent', () => {
|
||||
SharedModule,
|
||||
RouterTestingModule.withRoutes([]),
|
||||
TranslateModule.forRoot(),
|
||||
NoopAnimationsModule
|
||||
],
|
||||
declarations: [
|
||||
CommunityRolesComponent,
|
||||
|
@@ -18,7 +18,14 @@ describe('EditCommunityPageComponent', () => {
|
||||
dso: { payload: {} }
|
||||
}),
|
||||
routeConfig: {
|
||||
children: []
|
||||
children: [
|
||||
{
|
||||
path: 'mockUrl',
|
||||
data: {
|
||||
hideReturnButton: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
snapshot: {
|
||||
firstChild: {
|
||||
|
@@ -10,6 +10,7 @@ import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/res
|
||||
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
|
||||
import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver';
|
||||
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
|
||||
import { CommunityAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/community-administrator.guard';
|
||||
|
||||
/**
|
||||
* Routing module that handles the routing for the Edit Community page administrator functionality
|
||||
@@ -24,6 +25,7 @@ import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit
|
||||
},
|
||||
data: { breadcrumbKey: 'community.edit' },
|
||||
component: EditCommunityPageComponent,
|
||||
canActivate: [CommunityAdministratorGuard],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
|
26
src/app/+community-page/themed-community-page.component.ts
Normal file
26
src/app/+community-page/themed-community-page.component.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||
import { CommunityPageComponent } from './community-page.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for CommunityPageComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-community-page',
|
||||
styleUrls: [],
|
||||
templateUrl: '../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedCommunityPageComponent extends ThemedComponent<CommunityPageComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'CommunityPageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../themes/${themeName}/app/+community-page/community-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./community-page.component`);
|
||||
}
|
||||
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
<div class="jumbotron jumbotron-fluid">
|
||||
<div class="container">
|
||||
<div class="d-flex flex-wrap">
|
||||
<img class="mr-4 dspace-logo" src="assets/images/dspace-logo.svg" alt="" />
|
||||
<div>
|
||||
<h1 class="display-3">Welcome to the DSpace 7 Preview</h1>
|
||||
<p class="lead">DSpace is the world leading open source repository platform that enables organisations to:</p>
|
||||
|
@@ -8,7 +8,14 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.dspace-logo {
|
||||
height: 110px;
|
||||
width: 110px;
|
||||
.jumbotron {
|
||||
background-color: var(--ds-home-news-background-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--ds-home-news-link-color);
|
||||
|
||||
@include hover {
|
||||
color: var(--ds-home-news-link-hover-color);
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,6 @@
|
||||
<ng-container *ngIf="(site$ | async) as site">
|
||||
<ds-view-tracker [object]="site"></ds-view-tracker>
|
||||
</ng-container>
|
||||
<ds-search-form [inPlaceSearch]="false"></ds-search-form>
|
||||
<ds-search-form [inPlaceSearch]="false" [searchPlaceholder]="'home.search-form.placeholder' | translate"></ds-search-form>
|
||||
<ds-top-level-community-list></ds-top-level-community-list>
|
||||
</div>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { SubmissionImportExternalComponent } from '../submission/import-external/submission-import-external.component';
|
||||
import { ThemedSubmissionImportExternalComponent } from '../submission/import-external/themed-submission-import-external.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -9,7 +9,7 @@ import { SubmissionImportExternalComponent } from '../submission/import-external
|
||||
{
|
||||
canActivate: [ AuthenticatedGuard ],
|
||||
path: '',
|
||||
component: SubmissionImportExternalComponent,
|
||||
component: ThemedSubmissionImportExternalComponent,
|
||||
pathMatch: 'full',
|
||||
data: {
|
||||
title: 'submission.import-external.page.title'
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
|
||||
import {
|
||||
FieldUpdate,
|
||||
FieldUpdates
|
||||
@@ -30,7 +30,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
||||
/**
|
||||
* The item to display the edit page for
|
||||
*/
|
||||
item: Item;
|
||||
@Input() item: Item;
|
||||
/**
|
||||
* The current values and updates for all this item's fields
|
||||
* Should be initialized in the initializeUpdates method of the child component
|
||||
@@ -63,22 +63,24 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
||||
* Initialize common properties between item-update components
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.itemUpdateSubscription = observableCombineLatest([this.route.data, this.route.parent.data]).pipe(
|
||||
map(([data, parentData]: [Data, Data]) => Object.assign({}, data, parentData)),
|
||||
map((data: any) => data.dso),
|
||||
tap((rd: RemoteData<Item>) => {
|
||||
this.item = rd.payload;
|
||||
}),
|
||||
switchMap((rd: RemoteData<Item>) => {
|
||||
return this.itemService.findByHref(rd.payload._links.self.href, true, true, ...ITEM_PAGE_LINKS_TO_FOLLOW);
|
||||
}),
|
||||
getAllSucceededRemoteData()
|
||||
).subscribe((rd: RemoteData<Item>) => {
|
||||
this.item = rd.payload;
|
||||
this.itemPageRoute = getItemPageRoute(this.item);
|
||||
this.postItemInit();
|
||||
this.initializeUpdates();
|
||||
});
|
||||
if (hasValue(this.item)) {
|
||||
this.setItem(this.item);
|
||||
} else {
|
||||
// The item wasn't provided through an input, retrieve it from the route instead.
|
||||
this.itemUpdateSubscription = observableCombineLatest([this.route.data, this.route.parent.data]).pipe(
|
||||
map(([data, parentData]: [Data, Data]) => Object.assign({}, data, parentData)),
|
||||
map((data: any) => data.dso),
|
||||
tap((rd: RemoteData<Item>) => {
|
||||
this.item = rd.payload;
|
||||
}),
|
||||
switchMap((rd: RemoteData<Item>) => {
|
||||
return this.itemService.findByHref(rd.payload._links.self.href, true, true, ...ITEM_PAGE_LINKS_TO_FOLLOW);
|
||||
}),
|
||||
getAllSucceededRemoteData()
|
||||
).subscribe((rd: RemoteData<Item>) => {
|
||||
this.setItem(rd.payload);
|
||||
});
|
||||
}
|
||||
|
||||
this.discardTimeOut = environment.item.edit.undoTimeout;
|
||||
this.url = this.router.url;
|
||||
@@ -97,6 +99,13 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
||||
this.initializeUpdates();
|
||||
}
|
||||
|
||||
setItem(item: Item) {
|
||||
this.item = item;
|
||||
this.itemPageRoute = getItemPageRoute(this.item);
|
||||
this.postItemInit();
|
||||
this.initializeUpdates();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (hasValue(this.itemUpdateSubscription)) {
|
||||
this.itemUpdateSubscription.unsubscribe();
|
||||
|
@@ -5,18 +5,29 @@
|
||||
<div class="pt-2">
|
||||
<ul class="nav nav-tabs justify-content-start">
|
||||
<li *ngFor="let page of pages" class="nav-item">
|
||||
<a class="nav-link"
|
||||
[ngClass]="{'active' : page === currentPage}"
|
||||
[routerLink]="['./' + page]">
|
||||
{{'item.edit.tabs.' + page + '.head' | translate}}
|
||||
<a *ngIf="(page.enabled | async)"
|
||||
class="nav-link"
|
||||
[ngClass]="{'active' : page.page === currentPage}"
|
||||
[routerLink]="['./' + page.page]">
|
||||
{{'item.edit.tabs.' + page.page + '.head' | translate}}
|
||||
</a>
|
||||
<span [ngbTooltip]="'item.edit.tabs.disabled.tooltip' | translate">
|
||||
<button *ngIf="!(page.enabled | async)"
|
||||
class="nav-link disabled">
|
||||
{{'item.edit.tabs.' + page.page + '.head' | translate}}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-pane active">
|
||||
<div class="mb-4">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<a [routerLink]="getItemPage((itemRD$ | async)?.payload)" class="btn btn-outline-secondary">Cancel</a>
|
||||
<div class="button-row bottom">
|
||||
<div class="text-right">
|
||||
<a [routerLink]="getItemPage((itemRD$ | async)?.payload)" role="button" class="btn btn-outline-secondary mr-1"><i class="fas fa-arrow-left"></i> {{'item.edit.return' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,107 @@
|
||||
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { EditItemPageComponent } from './edit-item-page.component';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
|
||||
describe('ItemPageComponent', () => {
|
||||
let comp: EditItemPageComponent;
|
||||
let fixture: ComponentFixture<EditItemPageComponent>;
|
||||
|
||||
class AcceptAllGuard implements CanActivate {
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||
return observableOf(true);
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:max-classes-per-file
|
||||
class AcceptNoneGuard implements CanActivate {
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||
console.log('BLA');
|
||||
return observableOf(false);
|
||||
}
|
||||
}
|
||||
|
||||
const accesiblePages = ['accessible'];
|
||||
const inaccesiblePages = ['inaccessible', 'inaccessibleDoubleGuard'];
|
||||
const mockRoute = {
|
||||
snapshot: {
|
||||
firstChild: {
|
||||
routeConfig: {
|
||||
path: accesiblePages[0]
|
||||
}
|
||||
},
|
||||
routerState: {
|
||||
snapshot: undefined
|
||||
}
|
||||
},
|
||||
routeConfig: {
|
||||
children: [
|
||||
{
|
||||
path: accesiblePages[0],
|
||||
canActivate: [AcceptAllGuard]
|
||||
}, {
|
||||
path: inaccesiblePages[0],
|
||||
canActivate: [AcceptNoneGuard]
|
||||
}, {
|
||||
path: inaccesiblePages[1],
|
||||
canActivate: [AcceptAllGuard, AcceptNoneGuard]
|
||||
},
|
||||
]
|
||||
},
|
||||
data: observableOf({dso: createSuccessfulRemoteDataObject(new Item())})
|
||||
};
|
||||
|
||||
const mockRouter = {
|
||||
routerState: {
|
||||
snapshot: undefined
|
||||
},
|
||||
events: observableOf(undefined)
|
||||
};
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})],
|
||||
declarations: [EditItemPageComponent],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: mockRoute },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
AcceptAllGuard,
|
||||
AcceptNoneGuard,
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(EditItemPageComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(EditItemPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
spyOn((comp as any).injector, 'get').and.callFake((a) => new a());
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
describe('ngOnInit', () => {
|
||||
it('should enable tabs that the user can activate', fakeAsync(() => {
|
||||
const enabledItems = fixture.debugElement.queryAll(By.css('a.nav-link'));
|
||||
expect(enabledItems.length).toBe(accesiblePages.length);
|
||||
}));
|
||||
|
||||
it('should disable tabs that the user can not activate', () => {
|
||||
const disabledItems = fixture.debugElement.queryAll(By.css('button.nav-link.disabled'));
|
||||
expect(disabledItems.length).toBe(inaccesiblePages.length);
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,12 +1,13 @@
|
||||
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ChangeDetectionStrategy, Component, Injector, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, CanActivate, Route, Router } from '@angular/router';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { getItemPageRoute } from '../item-page-routing-paths';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-item-page',
|
||||
@@ -35,18 +36,29 @@ export class EditItemPageComponent implements OnInit {
|
||||
/**
|
||||
* All possible page outlet strings
|
||||
*/
|
||||
pages: string[];
|
||||
pages: { page: string, enabled: Observable<boolean> }[];
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router) {
|
||||
this.router.events.subscribe(() => {
|
||||
this.currentPage = this.route.snapshot.firstChild.routeConfig.path;
|
||||
});
|
||||
constructor(private route: ActivatedRoute, private router: Router, private injector: Injector) {
|
||||
this.router.events.subscribe(() => this.initPageParamsByRoute());
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initPageParamsByRoute();
|
||||
this.pages = this.route.routeConfig.children
|
||||
.map((child: any) => child.path)
|
||||
.filter((path: string) => isNotEmpty(path)); // ignore reroutes
|
||||
.filter((child: Route) => isNotEmpty(child.path))
|
||||
.map((child: Route) => {
|
||||
let enabled = observableOf(true);
|
||||
if (isNotEmpty(child.canActivate)) {
|
||||
enabled = observableCombineLatest(child.canActivate.map((guardConstructor: GenericConstructor<CanActivate>) => {
|
||||
const guard: CanActivate = this.injector.get<CanActivate>(guardConstructor);
|
||||
return guard.canActivate(this.route.snapshot, this.router.routerState.snapshot);
|
||||
})
|
||||
).pipe(
|
||||
map((canActivateOutcomes: any[]) => canActivateOutcomes.every((e) => e === true))
|
||||
);
|
||||
}
|
||||
return { page: child.path, enabled: enabled };
|
||||
}); // ignore reroutes
|
||||
this.itemRD$ = this.route.data.pipe(map((data) => data.dso));
|
||||
}
|
||||
|
||||
@@ -57,4 +69,11 @@ export class EditItemPageComponent implements OnInit {
|
||||
getItemPage(item: Item): string {
|
||||
return getItemPageRoute(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set page params depending on the route
|
||||
*/
|
||||
initPageParamsByRoute() {
|
||||
this.currentPage = this.route.snapshot.firstChild.routeConfig.path;
|
||||
}
|
||||
}
|
||||
|
@@ -22,15 +22,17 @@ import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit
|
||||
import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||
import {
|
||||
ITEM_EDIT_AUTHORIZATIONS_PATH,
|
||||
ITEM_EDIT_MOVE_PATH,
|
||||
ITEM_EDIT_DELETE_PATH,
|
||||
ITEM_EDIT_PUBLIC_PATH,
|
||||
ITEM_EDIT_MOVE_PATH,
|
||||
ITEM_EDIT_PRIVATE_PATH,
|
||||
ITEM_EDIT_PUBLIC_PATH,
|
||||
ITEM_EDIT_REINSTATE_PATH,
|
||||
ITEM_EDIT_WITHDRAW_PATH
|
||||
} from './edit-item-page.routing-paths';
|
||||
import { ItemPageReinstateGuard } from './item-page-reinstate.guard';
|
||||
import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||
import { ItemPageEditMetadataGuard } from '../item-page-edit-metadata.guard';
|
||||
import { ItemPageAdministratorGuard } from '../item-page-administrator.guard';
|
||||
|
||||
/**
|
||||
* Routing module that handles the routing for the Edit Item page administrator functionality
|
||||
@@ -57,22 +59,26 @@ import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||
{
|
||||
path: 'status',
|
||||
component: ItemStatusComponent,
|
||||
data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: 'bitstreams',
|
||||
component: ItemBitstreamsComponent,
|
||||
data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: 'metadata',
|
||||
component: ItemMetadataComponent,
|
||||
data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageEditMetadataGuard]
|
||||
},
|
||||
{
|
||||
path: 'relationships',
|
||||
component: ItemRelationshipsComponent,
|
||||
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageEditMetadataGuard]
|
||||
},
|
||||
/* TODO - uncomment & fix when view page exists
|
||||
{
|
||||
@@ -89,12 +95,14 @@ import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||
{
|
||||
path: 'versionhistory',
|
||||
component: ItemVersionHistoryComponent,
|
||||
data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: 'mapper',
|
||||
component: ItemCollectionMapperComponent,
|
||||
data: { title: 'item.edit.tabs.item-mapper.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.item-mapper.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -165,7 +173,9 @@ import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||
ResourcePolicyResolver,
|
||||
ResourcePolicyTargetResolver,
|
||||
ItemPageReinstateGuard,
|
||||
ItemPageWithdrawGuard
|
||||
ItemPageWithdrawGuard,
|
||||
ItemPageAdministratorGuard,
|
||||
ItemPageEditMetadataGuard,
|
||||
]
|
||||
})
|
||||
export class EditItemPageRoutingModule {
|
||||
|
@@ -5,22 +5,22 @@
|
||||
class="fas fa-upload"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.upload-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-danger mr-1" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async) || submitting"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-warning mr-1" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.reinstate-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || submitting"
|
||||
<button class="btn btn-primary mr-1" [disabled]="!(hasChanges() | async) || submitting"
|
||||
(click)="submit()"><i
|
||||
class="fas fa-save"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.save-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async) || submitting"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="item && bundles?.length > 0" class="container table-bordered mt-4">
|
||||
@@ -48,12 +48,6 @@
|
||||
|
||||
<div class="button-row bottom">
|
||||
<div class="mt-4 float-right">
|
||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async) || submitting"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i>
|
||||
@@ -64,6 +58,12 @@
|
||||
class="fas fa-save"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.save-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async) || submitting"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -29,6 +29,7 @@
|
||||
[query]="(searchOptions$ | async)?.query"
|
||||
[currentUrl]="'./'"
|
||||
[inPlaceSearch]="true"
|
||||
[searchPlaceholder]="'item.edit.item-mapper.search-form.placeholder' | translate"
|
||||
(submitSearch)="performedSearch = true">
|
||||
</ds-search-form>
|
||||
</div>
|
||||
|
@@ -173,6 +173,19 @@ describe('ItemDeleteComponent', () => {
|
||||
.toHaveBeenCalledWith(mockItem.id, types.filter((type) => typesSelection[type]).map((type) => type.id));
|
||||
expect(comp.notify).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call delete function from the ItemDataService with empty types', () => {
|
||||
|
||||
spyOn(comp, 'notify');
|
||||
jasmine.getEnv().allowRespy(true);
|
||||
spyOn(entityTypeService, 'getEntityTypeRelationships').and.returnValue([]);
|
||||
comp.ngOnInit();
|
||||
|
||||
comp.performAction();
|
||||
|
||||
expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem.id, []);
|
||||
expect(comp.notify).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('notify', () => {
|
||||
it('should navigate to the homepage on successful deletion of the item', () => {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { defaultIfEmpty, filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import {defaultIfEmpty, filter, map, switchMap, take} from 'rxjs/operators';
|
||||
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
@@ -112,7 +112,7 @@ export class ItemDeleteComponent
|
||||
super.ngOnInit();
|
||||
this.url = this.router.url;
|
||||
|
||||
const label = this.item.firstMetadataValue('relationship.type');
|
||||
const label = this.item.firstMetadataValue('dspace.entity.type');
|
||||
if (label !== undefined) {
|
||||
this.types$ = this.entityTypeService.getEntityTypeByLabel(label).pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
@@ -121,8 +121,11 @@ export class ItemDeleteComponent
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((relationshipTypes) => relationshipTypes.page),
|
||||
switchMap((types) =>
|
||||
combineLatest(types.map((type) => this.getRelationships(type))).pipe(
|
||||
switchMap((types) => {
|
||||
if (types.length === 0) {
|
||||
return observableOf(types);
|
||||
}
|
||||
return combineLatest(types.map((type) => this.getRelationships(type))).pipe(
|
||||
map((relationships) =>
|
||||
types.reduce<RelationshipType[]>((includedTypes, type, index) => {
|
||||
if (!includedTypes.some((includedType) => includedType.id === type.id)
|
||||
@@ -133,8 +136,8 @@ export class ItemDeleteComponent
|
||||
}
|
||||
}, [])
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.types$ = observableOf([]);
|
||||
|
@@ -5,12 +5,6 @@
|
||||
class="fas fa-plus"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.add-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i>
|
||||
@@ -21,6 +15,12 @@
|
||||
class="fas fa-save"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<table class="table table-responsive table-striped table-bordered" *ngIf="((updates$ | async)| dsObjectValues).length > 0">
|
||||
<tbody>
|
||||
@@ -47,20 +47,20 @@
|
||||
<ds-alert [content]="'item.edit.metadata.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
|
||||
</div>
|
||||
<div class="button-row bottom">
|
||||
<div class="float-right">
|
||||
<button class="btn btn-danger mr-1" *ngIf="!(isReinstatable() | async)"
|
||||
<div class="mt-2 float-right">
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
|
||||
</button>
|
||||
<button class="btn btn-primary mr-0" [disabled]="!(hasChanges() | async)"
|
||||
(click)="submit()"><i
|
||||
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
|
||||
</button>
|
||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
|
||||
</button>
|
||||
<button class="btn btn-warning mr-1" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
|
||||
</button>
|
||||
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
|
||||
(click)="submit()"><i
|
||||
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div *ngIf="!operation.disabled" class="col-9 float-left action-button">
|
||||
<a class="btn btn-outline-secondary" [routerLink]="operation.operationUrl">
|
||||
<a class="btn btn-outline-primary" [routerLink]="operation.operationUrl">
|
||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
||||
</a>
|
||||
</div>
|
||||
@@ -12,4 +12,4 @@
|
||||
<span class="btn btn-danger">
|
||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -75,7 +75,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
|
||||
*/
|
||||
public initializeUpdates(): void {
|
||||
|
||||
const label = this.item.firstMetadataValue('relationship.type');
|
||||
const label = this.item.firstMetadataValue('dspace.entity.type');
|
||||
if (label !== undefined) {
|
||||
|
||||
this.entityType$ = this.entityTypeService.getEntityTypeByLabel(label).pipe(
|
||||
|
@@ -18,9 +18,8 @@ import { hasValue } from '../../shared/empty.util';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
|
||||
/**
|
||||
* This component renders a simple item page.
|
||||
* This component renders a full item page.
|
||||
* The route parameter 'id' is used to request the item it represents.
|
||||
* All fields of the item that should be displayed, are defined in its template.
|
||||
*/
|
||||
|
||||
@Component({
|
||||
|
25
src/app/+item-page/full/themed-full-item-page.component.ts
Normal file
25
src/app/+item-page/full/themed-full-item-page.component.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { FullItemPageComponent } from './full-item-page.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for FullItemPageComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-full-item-page',
|
||||
styleUrls: [],
|
||||
templateUrl: './../../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedFullItemPageComponent extends ThemedComponent<FullItemPageComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'FullItemPageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/+item-page/full/full-item-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./full-item-page.component`);
|
||||
}
|
||||
}
|
31
src/app/+item-page/item-page-edit-metadata.guard.ts
Normal file
31
src/app/+item-page/item-page-edit-metadata.guard.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||
import { ItemPageResolver } from './item-page.resolver';
|
||||
import { Item } from '../core/shared/item.model';
|
||||
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
/**
|
||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring edit metadata rights
|
||||
*/
|
||||
export class ItemPageEditMetadataGuard extends DsoPageFeatureGuard<Item> {
|
||||
constructor(protected resolver: ItemPageResolver,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router,
|
||||
protected authService: AuthService) {
|
||||
super(resolver, authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check edit metadata authorization rights
|
||||
*/
|
||||
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||
return observableOf(FeatureID.CanEditMetadata);
|
||||
}
|
||||
}
|
@@ -10,11 +10,11 @@ export function getItemModuleRoute() {
|
||||
|
||||
/**
|
||||
* Get the route to an item's page
|
||||
* Depending on the item's relationship type, the route will either start with /items or /entities
|
||||
* Depending on the item's entity 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');
|
||||
const type = item.firstMetadataValue('dspace.entity.type');
|
||||
return getEntityPageRoute(type, item.uuid);
|
||||
}
|
||||
|
||||
|
@@ -1,18 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { ItemPageComponent } from './simple/item-page.component';
|
||||
import { FullItemPageComponent } from './full/full-item-page.component';
|
||||
import { ItemPageResolver } from './item-page.resolver';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { ItemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver';
|
||||
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||
import { LinkService } from '../core/cache/builders/link.service';
|
||||
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
||||
import { UPLOAD_BITSTREAM_PATH, ITEM_EDIT_PATH } from './item-page-routing-paths';
|
||||
import { ITEM_EDIT_PATH, UPLOAD_BITSTREAM_PATH } from './item-page-routing-paths';
|
||||
import { ItemPageAdministratorGuard } from './item-page-administrator.guard';
|
||||
import { MenuItemType } from '../shared/menu/initial-menus-state';
|
||||
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
||||
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -27,18 +26,17 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: ItemPageComponent,
|
||||
component: ThemedItemPageComponent,
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'full',
|
||||
component: FullItemPageComponent,
|
||||
component: ThemedFullItemPageComponent,
|
||||
},
|
||||
{
|
||||
path: ITEM_EDIT_PATH,
|
||||
loadChildren: () => import('./edit-item-page/edit-item-page.module')
|
||||
.then((m) => m.EditItemPageModule),
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: UPLOAD_BITSTREAM_PATH,
|
||||
@@ -68,7 +66,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
||||
ItemBreadcrumbResolver,
|
||||
DSOBreadcrumbsService,
|
||||
LinkService,
|
||||
ItemPageAdministratorGuard
|
||||
ItemPageAdministratorGuard,
|
||||
]
|
||||
|
||||
})
|
||||
|
@@ -25,6 +25,12 @@ import { AbstractIncrementalListComponent } from './simple/abstract-incremental-
|
||||
import { UntypedItemComponent } from './simple/item-types/untyped-item/untyped-item.component';
|
||||
import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module';
|
||||
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
|
||||
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
||||
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
|
||||
import { MediaViewerComponent } from './media-viewer/media-viewer.component';
|
||||
import { MediaViewerVideoComponent } from './media-viewer/media-viewer-video/media-viewer-video.component';
|
||||
import { MediaViewerImageComponent } from './media-viewer/media-viewer-image/media-viewer-image.component';
|
||||
import { NgxGalleryModule } from '@kolkov/ngx-gallery';
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put only entry components that use custom decorator
|
||||
@@ -34,7 +40,9 @@ const ENTRY_COMPONENTS = [
|
||||
|
||||
const DECLARATIONS = [
|
||||
ItemPageComponent,
|
||||
ThemedItemPageComponent,
|
||||
FullItemPageComponent,
|
||||
ThemedFullItemPageComponent,
|
||||
MetadataUriValuesComponent,
|
||||
ItemPageAuthorFieldComponent,
|
||||
ItemPageDateFieldComponent,
|
||||
@@ -50,6 +58,9 @@ const DECLARATIONS = [
|
||||
ItemComponent,
|
||||
UploadBitstreamComponent,
|
||||
AbstractIncrementalListComponent,
|
||||
MediaViewerComponent,
|
||||
MediaViewerVideoComponent,
|
||||
MediaViewerImageComponent
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
@@ -60,7 +71,8 @@ const DECLARATIONS = [
|
||||
EditItemPageModule,
|
||||
StatisticsModule.forRoot(),
|
||||
JournalEntitiesModule.withEntryComponents(),
|
||||
ResearchEntitiesModule.withEntryComponents()
|
||||
ResearchEntitiesModule.withEntryComponents(),
|
||||
NgxGalleryModule,
|
||||
],
|
||||
declarations: [
|
||||
...DECLARATIONS
|
||||
|
@@ -0,0 +1,7 @@
|
||||
<div [class.change-gallery]="isAuthenticated$ | async">
|
||||
<ngx-gallery
|
||||
class="ngx-gallery"
|
||||
[options]="galleryOptions"
|
||||
[images]="galleryImages"
|
||||
></ngx-gallery>
|
||||
</div>
|
@@ -0,0 +1,6 @@
|
||||
.ngx-gallery {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
width: 340px !important;
|
||||
height: 279px !important;
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { NgxGalleryOptions } from '@kolkov/ngx-gallery';
|
||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
|
||||
import { MockBitstreamFormat1 } from '../../../shared/mocks/item.mock';
|
||||
|
||||
import { MediaViewerImageComponent } from './media-viewer-image.component';
|
||||
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { AuthService } from '../../../core/auth/auth.service';
|
||||
|
||||
describe('MediaViewerImageComponent', () => {
|
||||
let component: MediaViewerImageComponent;
|
||||
let fixture: ComponentFixture<MediaViewerImageComponent>;
|
||||
|
||||
const authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(false)
|
||||
});
|
||||
|
||||
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
|
||||
sizeBytes: 10201,
|
||||
content:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||
format: observableOf(MockBitstreamFormat1),
|
||||
bundleName: 'ORIGINAL',
|
||||
_links: {
|
||||
self: {
|
||||
href:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
},
|
||||
content: {
|
||||
href:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||
},
|
||||
},
|
||||
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
type: 'bitstream',
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: null,
|
||||
value: 'test_word.docx',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const mockMediaViewerItems: MediaViewerItem[] = Object.assign(
|
||||
new Array<MediaViewerItem>(),
|
||||
[
|
||||
{ bitstream: mockBitstream, format: 'image', thumbnail: null },
|
||||
{ bitstream: mockBitstream, format: 'image', thumbnail: null },
|
||||
]
|
||||
);
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports:[],
|
||||
declarations: [MediaViewerImageComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
providers: [
|
||||
{ provide: AuthService, useValue: authService },
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MediaViewerImageComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.galleryOptions = [new NgxGalleryOptions({})];
|
||||
component.galleryImages = component.convertToGalleryImage(
|
||||
mockMediaViewerItems
|
||||
);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should contain a gallery options', () => {
|
||||
expect(component.galleryOptions.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should contain an image array', () => {
|
||||
expect(component.galleryImages.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
@@ -0,0 +1,88 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { NgxGalleryImage, NgxGalleryOptions } from '@kolkov/ngx-gallery';
|
||||
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
|
||||
import { NgxGalleryAnimation } from '@kolkov/ngx-gallery';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AuthService } from '../../../core/auth/auth.service';
|
||||
|
||||
/**
|
||||
* This componenet render an image gallery for the image viewer
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-media-viewer-image',
|
||||
templateUrl: './media-viewer-image.component.html',
|
||||
styleUrls: ['./media-viewer-image.component.scss'],
|
||||
})
|
||||
export class MediaViewerImageComponent implements OnInit {
|
||||
@Input() images: MediaViewerItem[];
|
||||
@Input() preview?: boolean;
|
||||
@Input() image?: string;
|
||||
|
||||
loggedin: boolean;
|
||||
|
||||
galleryOptions: NgxGalleryOptions[];
|
||||
galleryImages: NgxGalleryImage[];
|
||||
|
||||
/**
|
||||
* Whether or not the current user is authenticated
|
||||
*/
|
||||
isAuthenticated$: Observable<boolean>;
|
||||
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
/**
|
||||
* Thi method sets up the gallery settings and data
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.isAuthenticated$ = this.authService.isAuthenticated();
|
||||
this.galleryOptions = [
|
||||
{
|
||||
preview: this.preview !== undefined ? this.preview : true,
|
||||
image: true,
|
||||
imageSize: 'contain',
|
||||
thumbnails: false,
|
||||
imageArrows: false,
|
||||
startIndex: 0,
|
||||
imageAnimation: NgxGalleryAnimation.Slide,
|
||||
previewCloseOnEsc: true,
|
||||
previewZoom: true,
|
||||
previewRotate: true,
|
||||
previewFullscreen: true,
|
||||
},
|
||||
];
|
||||
|
||||
if (this.image) {
|
||||
this.galleryImages = [
|
||||
{
|
||||
small: this.image,
|
||||
medium: this.image,
|
||||
big: this.image,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
this.galleryImages = this.convertToGalleryImage(this.images);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method convert an array of MediaViewerItem into NgxGalleryImage array
|
||||
* @param medias input NgxGalleryImage array
|
||||
*/
|
||||
convertToGalleryImage(medias: MediaViewerItem[]): NgxGalleryImage[] {
|
||||
const mappadImages = [];
|
||||
for (const image of medias) {
|
||||
if (image.format === 'image') {
|
||||
mappadImages.push({
|
||||
small: image.thumbnail
|
||||
? image.thumbnail
|
||||
: './assets/images/replacement_image.svg',
|
||||
medium: image.thumbnail
|
||||
? image.thumbnail
|
||||
: './assets/images/replacement_image.svg',
|
||||
big: image.bitstream._links.content.href,
|
||||
});
|
||||
}
|
||||
}
|
||||
return mappadImages;
|
||||
}
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
<video
|
||||
#media
|
||||
[src]="filteredMedias[currentIndex].bitstream._links.content.href"
|
||||
id="singleVideo"
|
||||
[poster]="
|
||||
filteredMedias[currentIndex].thumbnail ||
|
||||
replacements[filteredMedias[currentIndex].format]
|
||||
"
|
||||
preload="none"
|
||||
controls
|
||||
></video>
|
||||
<div class="buttons" *ngIf="filteredMedias?.length > 1">
|
||||
<button
|
||||
class="btn btn-primary previous"
|
||||
[disabled]="currentIndex === 0"
|
||||
(click)="prevMedia()"
|
||||
>
|
||||
{{ "media-viewer.previous" | translate }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-primary next"
|
||||
[disabled]="currentIndex === filteredMedias.length - 1"
|
||||
(click)="nextMedia()"
|
||||
>
|
||||
{{ "media-viewer.next" | translate }}
|
||||
</button>
|
||||
<div ngbDropdown class="d-inline-block">
|
||||
<button
|
||||
class="btn btn-outline-primary playlist"
|
||||
id="dropdownBasic1"
|
||||
ngbDropdownToggle
|
||||
>
|
||||
{{ "media-viewer.playlist" | translate }}
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
|
||||
<button
|
||||
ngbDropdownItem
|
||||
*ngFor="let item of filteredMedias; index as indexOfelement"
|
||||
class="list-element"
|
||||
(click)="selectedMedia(indexOfelement)"
|
||||
>
|
||||
{{ item.bitstream.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,4 @@
|
||||
video {
|
||||
width: 340px;
|
||||
height: 279px;
|
||||
}
|
@@ -0,0 +1,145 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
|
||||
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
||||
import { FileSizePipe } from '../../../shared/utils/file-size-pipe';
|
||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||
import { MetadataFieldWrapperComponent } from '../../field-components/metadata-field-wrapper/metadata-field-wrapper.component';
|
||||
import { MockBitstreamFormat1 } from '../../../shared/mocks/item.mock';
|
||||
import { MediaViewerVideoComponent } from './media-viewer-video.component';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('MediaViewerVideoComponent', () => {
|
||||
let component: MediaViewerVideoComponent;
|
||||
let fixture: ComponentFixture<MediaViewerVideoComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock,
|
||||
},
|
||||
}),
|
||||
BrowserAnimationsModule,
|
||||
],
|
||||
declarations: [
|
||||
MediaViewerVideoComponent,
|
||||
VarDirective,
|
||||
FileSizePipe,
|
||||
MetadataFieldWrapperComponent,
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
|
||||
sizeBytes: 10201,
|
||||
content:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||
format: observableOf(MockBitstreamFormat1),
|
||||
bundleName: 'ORIGINAL',
|
||||
_links: {
|
||||
self: {
|
||||
href:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
},
|
||||
content: {
|
||||
href:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||
},
|
||||
},
|
||||
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
type: 'bitstream',
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: null,
|
||||
value: 'test_word.docx',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const mockMediaViewerItems: MediaViewerItem[] = Object.assign(
|
||||
new Array<MediaViewerItem>(),
|
||||
[
|
||||
{ bitstream: mockBitstream, format: 'video', thumbnail: null },
|
||||
{ bitstream: mockBitstream, format: 'video', thumbnail: null },
|
||||
]
|
||||
);
|
||||
const mockMediaViewerItem: MediaViewerItem[] = Object.assign(
|
||||
new Array<MediaViewerItem>(),
|
||||
[{ bitstream: mockBitstream, format: 'video', thumbnail: null }]
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MediaViewerVideoComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.medias = mockMediaViewerItem;
|
||||
component.filteredMedias = mockMediaViewerItem;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('should show controller buttons when the having mode then one video', () => {
|
||||
beforeEach(() => {
|
||||
component.medias = mockMediaViewerItems;
|
||||
component.filteredMedias = mockMediaViewerItems;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show buttons', () => {
|
||||
const controllerButtons = fixture.debugElement.query(By.css('.buttons'));
|
||||
expect(controllerButtons).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when the "Next" button is clicked', () => {
|
||||
beforeEach(() => {
|
||||
component.currentIndex = 0;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should increase the index', () => {
|
||||
const viewMore = fixture.debugElement.query(By.css('.next'));
|
||||
viewMore.triggerEventHandler('click', null);
|
||||
expect(component.currentIndex).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the "Previous" button is clicked', () => {
|
||||
beforeEach(() => {
|
||||
component.currentIndex = 1;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should decrease the index', () => {
|
||||
const viewMore = fixture.debugElement.query(By.css('.previous'));
|
||||
viewMore.triggerEventHandler('click', null);
|
||||
expect(component.currentIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the "Playlist element" button is clicked', () => {
|
||||
beforeEach(() => {
|
||||
component.isCollapsed = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should set the the index with the selected one', () => {
|
||||
const viewMore = fixture.debugElement.query(By.css('.list-element'));
|
||||
viewMore.triggerEventHandler('click', null);
|
||||
expect(component.currentIndex).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,55 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
|
||||
|
||||
/**
|
||||
* This componenet renders a video viewer and playlist for the media viewer
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-media-viewer-video',
|
||||
templateUrl: './media-viewer-video.component.html',
|
||||
styleUrls: ['./media-viewer-video.component.scss'],
|
||||
})
|
||||
export class MediaViewerVideoComponent implements OnInit {
|
||||
@Input() medias: MediaViewerItem[];
|
||||
|
||||
filteredMedias: MediaViewerItem[];
|
||||
|
||||
isCollapsed: boolean;
|
||||
currentIndex = 0;
|
||||
|
||||
replacements = {
|
||||
video: './assets/images/replacement_video.svg',
|
||||
audio: './assets/images/replacement_audio.svg',
|
||||
};
|
||||
|
||||
replacementThumbnail: string;
|
||||
|
||||
ngOnInit() {
|
||||
this.isCollapsed = false;
|
||||
this.filteredMedias = this.medias.filter(
|
||||
(media) => media.format === 'audio' || media.format === 'video'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method sets the reviced index into currentIndex
|
||||
* @param index Selected index
|
||||
*/
|
||||
selectedMedia(index: number) {
|
||||
this.currentIndex = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method increade the number of the currentIndex
|
||||
*/
|
||||
nextMedia() {
|
||||
this.currentIndex++;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method decrese the number of the currentIndex
|
||||
*/
|
||||
prevMedia() {
|
||||
this.currentIndex--;
|
||||
}
|
||||
}
|
36
src/app/+item-page/media-viewer/media-viewer.component.html
Normal file
36
src/app/+item-page/media-viewer/media-viewer.component.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<ng-container *ngVar="mediaList$ | async as mediaList">
|
||||
<ds-loading
|
||||
*ngIf="isLoading"
|
||||
message="{{ 'loading.default' | translate }}"
|
||||
[showMessage]="false"
|
||||
></ds-loading>
|
||||
<div class="media-viewer" *ngIf="!isLoading">
|
||||
<ng-container *ngIf="mediaList.length > 0">
|
||||
<ng-container *ngIf="videoOptions">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
mediaList[0]?.format === 'video' || mediaList[0]?.format === 'audio'
|
||||
"
|
||||
>
|
||||
<ds-media-viewer-video [medias]="mediaList"></ds-media-viewer-video>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="mediaList[0]?.format === 'image'">
|
||||
<ds-media-viewer-image [images]="mediaList"></ds-media-viewer-image>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container
|
||||
*ngIf="
|
||||
((mediaList[0]?.format !== 'image') &&
|
||||
(!videoOptions || mediaList[0]?.format !== 'video') &&
|
||||
(!videoOptions || mediaList[0]?.format !== 'audio')) ||
|
||||
mediaList.length === 0
|
||||
"
|
||||
>
|
||||
<ds-media-viewer-image
|
||||
[image]="mediaList[0]?.thumbnail || thumbnailPlaceholder"
|
||||
[preview]="false"
|
||||
></ds-media-viewer-image>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
@@ -0,0 +1 @@
|
||||
|
143
src/app/+item-page/media-viewer/media-viewer.component.spec.ts
Normal file
143
src/app/+item-page/media-viewer/media-viewer.component.spec.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { MediaViewerComponent } from './media-viewer.component';
|
||||
import { MockBitstreamFormat1 } from '../../shared/mocks/item.mock';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { MediaViewerItem } from '../../core/shared/media-viewer-item.model';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { MetadataFieldWrapperComponent } from '../field-components/metadata-field-wrapper/metadata-field-wrapper.component';
|
||||
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
||||
|
||||
describe('MediaViewerComponent', () => {
|
||||
let comp: MediaViewerComponent;
|
||||
let fixture: ComponentFixture<MediaViewerComponent>;
|
||||
|
||||
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
|
||||
sizeBytes: 10201,
|
||||
content:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||
format: observableOf(MockBitstreamFormat1),
|
||||
bundleName: 'ORIGINAL',
|
||||
_links: {
|
||||
self: {
|
||||
href:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
},
|
||||
content: {
|
||||
href:
|
||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||
},
|
||||
},
|
||||
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
type: 'bitstream',
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: null,
|
||||
value: 'test_word.docx',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
||||
findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(
|
||||
createPaginatedList([mockBitstream])
|
||||
),
|
||||
});
|
||||
|
||||
const mockMediaViewerItem: MediaViewerItem = Object.assign(
|
||||
new MediaViewerItem(),
|
||||
{ bitstream: mockBitstream, format: 'image', thumbnail: null }
|
||||
);
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock,
|
||||
},
|
||||
}),
|
||||
BrowserAnimationsModule,
|
||||
],
|
||||
declarations: [
|
||||
MediaViewerComponent,
|
||||
VarDirective,
|
||||
FileSizePipe,
|
||||
MetadataFieldWrapperComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MediaViewerComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('when the bitstreams are loading', () => {
|
||||
beforeEach(() => {
|
||||
comp.mediaList$.next([mockMediaViewerItem]);
|
||||
comp.videoOptions = true;
|
||||
comp.isLoading = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should call the createMediaViewerItem', () => {
|
||||
const mediaItem = comp.createMediaViewerItem(
|
||||
mockBitstream,
|
||||
MockBitstreamFormat1,
|
||||
undefined
|
||||
);
|
||||
expect(mediaItem).toBeTruthy();
|
||||
expect(mediaItem.thumbnail).toBe(null);
|
||||
});
|
||||
|
||||
it('should display a loading component', () => {
|
||||
const loading = fixture.debugElement.query(By.css('ds-loading'));
|
||||
expect(loading.nativeElement).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the bitstreams loading is failed', () => {
|
||||
beforeEach(() => {
|
||||
comp.mediaList$.next([]);
|
||||
comp.videoOptions = true;
|
||||
comp.isLoading = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should call the createMediaViewerItem', () => {
|
||||
const mediaItem = comp.createMediaViewerItem(
|
||||
mockBitstream,
|
||||
MockBitstreamFormat1,
|
||||
undefined
|
||||
);
|
||||
expect(mediaItem).toBeTruthy();
|
||||
expect(mediaItem.thumbnail).toBe(null);
|
||||
});
|
||||
|
||||
it('should display a default, thumbnail', () => {
|
||||
const defaultThumbnail = fixture.debugElement.query(
|
||||
By.css('ds-media-viewer-image')
|
||||
);
|
||||
expect(defaultThumbnail.nativeElement).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
114
src/app/+item-page/media-viewer/media-viewer.component.ts
Normal file
114
src/app/+item-page/media-viewer/media-viewer.component.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { filter, take } from 'rxjs/operators';
|
||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { MediaViewerItem } from '../../core/shared/media-viewer-item.model';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
|
||||
/**
|
||||
* This componenet renders the media viewers
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'ds-media-viewer',
|
||||
templateUrl: './media-viewer.component.html',
|
||||
styleUrls: ['./media-viewer.component.scss'],
|
||||
})
|
||||
export class MediaViewerComponent implements OnInit {
|
||||
@Input() item: Item;
|
||||
@Input() videoOptions: boolean;
|
||||
|
||||
mediaList$: BehaviorSubject<MediaViewerItem[]>;
|
||||
|
||||
isLoading: boolean;
|
||||
|
||||
thumbnailPlaceholder = './assets/images/replacement_document.svg';
|
||||
|
||||
constructor(protected bitstreamDataService: BitstreamDataService) {}
|
||||
|
||||
/**
|
||||
* This metod loads all the Bitstreams and Thumbnails and contert it to media item
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.mediaList$ = new BehaviorSubject([]);
|
||||
this.isLoading = true;
|
||||
this.loadRemoteData('ORIGINAL').subscribe((bitstreamsRD) => {
|
||||
if (bitstreamsRD.payload.page.length === 0) {
|
||||
this.isLoading = false;
|
||||
this.mediaList$.next([]);
|
||||
} else {
|
||||
this.loadRemoteData('THUMBNAIL').subscribe((thumbnailsRD) => {
|
||||
for (
|
||||
let index = 0;
|
||||
index < bitstreamsRD.payload.page.length;
|
||||
index++
|
||||
) {
|
||||
bitstreamsRD.payload.page[index].format
|
||||
.pipe(getFirstSucceededRemoteDataPayload())
|
||||
.subscribe((format) => {
|
||||
const current = this.mediaList$.getValue();
|
||||
const mediaItem = this.createMediaViewerItem(
|
||||
bitstreamsRD.payload.page[index],
|
||||
format,
|
||||
thumbnailsRD.payload && thumbnailsRD.payload.page[index]
|
||||
);
|
||||
this.mediaList$.next([...current, mediaItem]);
|
||||
});
|
||||
}
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will retrieve the next page of Bitstreams from the external BitstreamDataService call.
|
||||
* @param bundleName Bundle name
|
||||
*/
|
||||
loadRemoteData(
|
||||
bundleName: string
|
||||
): Observable<RemoteData<PaginatedList<Bitstream>>> {
|
||||
return this.bitstreamDataService
|
||||
.findAllByItemAndBundleName(
|
||||
this.item,
|
||||
bundleName,
|
||||
{},
|
||||
true,
|
||||
true,
|
||||
followLink('format')
|
||||
)
|
||||
.pipe(
|
||||
filter(
|
||||
(bitstreamsRD: RemoteData<PaginatedList<Bitstream>>) =>
|
||||
hasValue(bitstreamsRD) &&
|
||||
(hasValue(bitstreamsRD.errorMessage) || hasValue(bitstreamsRD.payload))
|
||||
),
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method create MediaViewerItem from incoming bitstreams
|
||||
* @param original original remote data bitstream
|
||||
* @param format original bitstream format
|
||||
* @param thumbnail trunbnail remote data bitstream
|
||||
*/
|
||||
createMediaViewerItem(
|
||||
original: Bitstream,
|
||||
format: BitstreamFormat,
|
||||
thumbnail: Bitstream
|
||||
): MediaViewerItem {
|
||||
const mediaItem = new MediaViewerItem();
|
||||
mediaItem.bitstream = original;
|
||||
mediaItem.format = format.mimetype.split('/')[0];
|
||||
mediaItem.thumbnail = thumbnail ? thumbnail._links.content.href : null;
|
||||
return mediaItem;
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
<h2 class="item-page-title-field">
|
||||
<div *ngIf="item.firstMetadataValue('relationship.type') as type">
|
||||
<div *ngIf="item.firstMetadataValue('dspace.entity.type') as type">
|
||||
{{ type.toLowerCase() + '.page.titleprefix' | translate }}
|
||||
</div>
|
||||
<ds-metadata-values [mdValues]="item?.allMetadata(fields)"></ds-metadata-values>
|
||||
|
@@ -8,9 +8,14 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-4">
|
||||
<ds-metadata-field-wrapper>
|
||||
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
|
||||
</ds-metadata-field-wrapper>
|
||||
<ng-container *ngIf="!mediaViewer.image">
|
||||
<ds-metadata-field-wrapper>
|
||||
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
|
||||
</ds-metadata-field-wrapper>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="mediaViewer.image">
|
||||
<ds-media-viewer [item]="object" [videoOptions]="mediaViewer.video"></ds-media-viewer>
|
||||
</ng-container>
|
||||
<ds-item-page-file-section [item]="object"></ds-item-page-file-section>
|
||||
<ds-item-page-date-field [item]="object"></ds-item-page-date-field>
|
||||
<ds-item-page-author-field [item]="object"></ds-item-page-author-field>
|
||||
@@ -74,8 +79,8 @@
|
||||
</ds-item-page-uri-field>
|
||||
<ds-item-page-collections [item]="object"></ds-item-page-collections>
|
||||
<div>
|
||||
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']">
|
||||
{{"item.page.link.full" | translate}}
|
||||
<a class="btn btn-outline-primary" role="button" [routerLink]="[itemPageRoute + '/full']">
|
||||
<i class="fas fa-info-circle"></i> {{"item.page.link.full" | translate}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { environment } from '../../../../../environments/environment';
|
||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
@@ -20,6 +21,7 @@ export class ItemComponent implements OnInit {
|
||||
* Route to the item page
|
||||
*/
|
||||
itemPageRoute: string;
|
||||
mediaViewer = environment.mediaViewer;
|
||||
|
||||
constructor(protected bitstreamDataService: BitstreamDataService) {
|
||||
}
|
||||
|
@@ -8,9 +8,14 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-4">
|
||||
<ds-metadata-field-wrapper>
|
||||
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
|
||||
</ds-metadata-field-wrapper>
|
||||
<ng-container *ngIf="!mediaViewer.image">
|
||||
<ds-metadata-field-wrapper>
|
||||
<ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail>
|
||||
</ds-metadata-field-wrapper>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="mediaViewer.image">
|
||||
<ds-media-viewer [item]="object" [videoOptions]="mediaViewer.video"></ds-media-viewer>
|
||||
</ng-container>
|
||||
<ds-item-page-file-section [item]="object"></ds-item-page-file-section>
|
||||
<ds-item-page-date-field [item]="object"></ds-item-page-date-field>
|
||||
<ds-item-page-author-field [item]="object"></ds-item-page-author-field>
|
||||
@@ -59,8 +64,8 @@
|
||||
</ds-item-page-uri-field>
|
||||
<ds-item-page-collections [item]="object"></ds-item-page-collections>
|
||||
<div>
|
||||
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']">
|
||||
{{"item.page.link.full" | translate}}
|
||||
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button">
|
||||
<i class="fas fa-info-circle"></i> {{"item.page.link.full" | translate}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
27
src/app/+item-page/simple/themed-item-page.component.ts
Normal file
27
src/app/+item-page/simple/themed-item-page.component.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { ItemPageComponent } from './item-page.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for ItemPageComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-item-page',
|
||||
styleUrls: [],
|
||||
templateUrl: './../../shared/theme-support/themed.component.html',
|
||||
})
|
||||
|
||||
export class ThemedItemPageComponent extends ThemedComponent<ItemPageComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'ItemPageComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/+item-page/simple/item-page.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./item-page.component`);
|
||||
}
|
||||
|
||||
}
|
@@ -1,14 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { LoginPageComponent } from './login-page.component';
|
||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||
import { ThemedLoginPageComponent } from './themed-login-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{ path: '', pathMatch: 'full', component: LoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' } }
|
||||
{ path: '', pathMatch: 'full', component: ThemedLoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' } }
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="container w-100 h-100">
|
||||
<div class="text-center mt-5 row justify-content-center">
|
||||
<div>
|
||||
<img class="mb-4 login-logo" src="assets/images/dspace-logo.png">
|
||||
<img class="mb-4 login-logo" src="assets/images/dspace-logo.png" alt="{{'repository.image.logo' | translate}}">
|
||||
<h1 class="h3 mb-0 font-weight-normal">{{"login.form.header" | translate}}</h1>
|
||||
<ds-log-in
|
||||
[isStandalonePage]="true"></ds-log-in>
|
||||
|
@@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { LoginPageComponent } from './login-page.component';
|
||||
import { LoginPageRoutingModule } from './login-page-routing.module';
|
||||
import { ThemedLoginPageComponent } from './themed-login-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -11,7 +12,8 @@ import { LoginPageRoutingModule } from './login-page-routing.module';
|
||||
SharedModule,
|
||||
],
|
||||
declarations: [
|
||||
LoginPageComponent
|
||||
LoginPageComponent,
|
||||
ThemedLoginPageComponent
|
||||
]
|
||||
})
|
||||
export class LoginPageModule {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user