diff --git a/config/environment.default.js b/config/environment.default.js
index 2926440338..001fa4a136 100644
--- a/config/environment.default.js
+++ b/config/environment.default.js
@@ -52,5 +52,25 @@ module.exports = {
// Log directory
logDirectory: '.',
// NOTE: will log all redux actions and transfers in console
- debug: false
+ debug: false,
+ // Default Language in which the UI will be rendered if the user's browser language is not an active language
+ defaultLanguage: 'en',
+ // Languages. DSpace Angular holds a message catalog for each of the following languages. When set to active, users will be able to switch to the use of this language in the user interface.
+ languages: [{
+ code: 'en',
+ label: 'English',
+ active: true,
+ }, {
+ code: 'de',
+ label: 'Deutsch',
+ active: true,
+ }, {
+ code: 'cs',
+ label: 'Čeština',
+ active: true,
+ }, {
+ code: 'nl',
+ label: 'Nederlands',
+ active: false,
+ }]
};
diff --git a/package.json b/package.json
index 91117723d1..bf9741c870 100644
--- a/package.json
+++ b/package.json
@@ -130,6 +130,7 @@
"devDependencies": {
"@angular/compiler": "^6.1.4",
"@angular/compiler-cli": "^6.1.4",
+ "@fortawesome/fontawesome-free": "^5.5.0",
"@ngrx/entity": "^6.1.0",
"@ngrx/schematics": "^6.1.0",
"@ngrx/store-devtools": "^6.1.0",
@@ -182,7 +183,7 @@
"karma-webdriver-launcher": "1.0.5",
"karma-webpack": "3.0.0",
"ngrx-store-freeze": "^0.2.4",
- "node-sass": "^4.7.2",
+ "node-sass": "^4.11.0",
"nodemon": "^1.15.0",
"npm-run-all": "4.1.3",
"postcss": "^7.0.2",
diff --git a/resources/i18n/en.json b/resources/i18n/en.json
index e3126b16b8..e296326b72 100644
--- a/resources/i18n/en.json
+++ b/resources/i18n/en.json
@@ -22,6 +22,9 @@
},
"sub-collection-list": {
"head": "Collections of this Community"
+ },
+ "sub-community-list": {
+ "head": "Communities of this Community"
}
},
"item": {
@@ -43,6 +46,120 @@
"simple": "Simple item page",
"full": "Full item page"
}
+ },
+ "select": {
+ "table": {
+ "collection": "Collection",
+ "author": "Author",
+ "title": "Title"
+ },
+ "confirm": "Confirm selected"
+ },
+ "edit": {
+ "head": "Edit Item",
+ "tabs": {
+ "status": {
+ "head": "Item Status",
+ "description": "Welcome to the item management page. From here you can withdraw, reinstate, move or delete the item. You may also update or add new metadata / bitstreams on the other tabs.",
+ "labels": {
+ "id": "Item Internal ID",
+ "handle": "Handle",
+ "lastModified": "Last Modified",
+ "itemPage": "Item Page"
+ },
+ "buttons": {
+ "authorizations": {
+ "label": "Edit item's authorization policies",
+ "button": "Authorizations..."
+ },
+ "withdraw": {
+ "label": "Withdraw item from the repository",
+ "button": "Withdraw..."
+ },
+ "reinstate": {
+ "label": "Reinstate item into the repository",
+ "button": "Reinstate..."
+ },
+ "move": {
+ "label": "Move item to another collection",
+ "button": "Move..."
+ },
+ "private": {
+ "label": "Make item private",
+ "button": "Make it private..."
+ },
+ "public": {
+ "label": "Make item public",
+ "button": "Make it public..."
+ },
+ "delete": {
+ "label": "Completely expunge item",
+ "button": "Permanently delete"
+ },
+ "mappedCollections": {
+ "label": "Manage mapped collections",
+ "button": "Mapped collections"
+ }
+ }
+ },
+ "bitstreams": {
+ "head": "Item Bitstreams"
+ },
+ "metadata": {
+ "head": "Item Metadata"
+ },
+ "view": {
+ "head": "View Item"
+ },
+ "curate": {
+ "head": "Curate"
+ }
+ },
+ "modify.overview": {
+ "field": "Field",
+ "value": "Value",
+ "language": "Language"
+ },
+ "withdraw": {
+ "header": "Withdraw item: {{ id }}",
+ "description": "Are you sure this item should be withdrawn from the archive?",
+ "confirm": "Withdraw",
+ "cancel": "Cancel",
+ "success": "The item was withdrawn successfully",
+ "error": "An error occured while withdrawing the item"
+ },
+ "reinstate": {
+ "header": "Reinstate item: {{ id }}",
+ "description": "Are you sure this item should be reinstated to the archive?",
+ "confirm": "Reinstate",
+ "cancel": "Cancel",
+ "success": "The item was reinstated successfully",
+ "error": "An error occured while reinstating the item"
+ },
+ "private": {
+ "header": "Make item private: {{ id }}",
+ "description": "Are you sure this item should be made private in the archive?",
+ "confirm": "Make it Private",
+ "cancel": "Cancel",
+ "success": "The item is now private",
+ "error": "An error occured while making the item private"
+ },
+ "public": {
+ "header": "Make item public: {{ id }}",
+ "description": "Are you sure this item should be made public in the archive?",
+ "confirm": "Make it Public",
+ "cancel": "Cancel",
+ "success": "The item is now public",
+ "error": "An error occured while making the item public"
+ },
+ "delete": {
+ "header": "Delete item: {{ id }}",
+ "description": "Are you sure this item should be completely deleted? Caution: At present, no tombstone would be left.",
+ "confirm": "Delete",
+ "cancel": "Cancel",
+ "success": "The item has been deleted",
+ "error": "An error occured while deleting the item"
+ }
}
},
"relationships": {
@@ -114,9 +231,19 @@
}
},
"nav": {
- "home": "Home",
+ "browse": {
+ "header": "All of DSpace"
+ },
+ "community-browse": {
+ "header": "By Community"
+ },
+ "statistics": {
+ "header": "Statistics"
+ },
"login": "Log In",
- "logout": "Log Out"
+ "logout": "Log Out",
+ "language": "Language switch",
+ "search": "Search"
},
"pagination": {
"results-per-page": "Results Per Page",
@@ -295,12 +422,92 @@
}
}
},
+ "menu": {
+ "header": {
+ "admin": "Admin",
+ "image": {
+ "logo": "Repository logo"
+ }
+ },
+ "section": {
+ "pin": "Pin sidebar",
+ "unpin": "Unpin sidebar",
+ "new": "New",
+ "new_community": "Community",
+ "new_collection": "Collection",
+ "new_item": "Item",
+ "new_item_version": "Item Version",
+ "edit": "Edit",
+ "edit_community": "Community",
+ "edit_collection": "Collection",
+ "edit_item": "Item",
+ "import": "Import",
+ "import_metadata": "Metadata",
+ "import_batch": "Batch Import (ZIP)",
+ "export": "Export",
+ "export_community": "Community",
+ "export_collection": "Collection",
+ "export_item": "Item",
+ "export_metadata": "Metadata",
+ "access_control": "Access Control",
+ "access_control_people": "People",
+ "access_control_groups": "Groups",
+ "access_control_authorizations": "Authorizations",
+ "find": "Find",
+ "find_items": "Items",
+ "find_withdrawn_items": "Withdrawn Items",
+ "find_private_items": "Private Items",
+ "registries": "Registries",
+ "registries_metadata": "Metadata",
+ "registries_format": "Format",
+ "curation_task": "Curation Task",
+ "statistics_task": "Statistics Task",
+ "control_panel": "Control Panel",
+ "browse_global": "All of DSpace",
+ "browse_global_communities_and_collections": "Communities & Collections",
+ "browse_global_by_issue_date": "By Issue Date",
+ "browse_global_by_author": "By Author",
+ "browse_global_by_title": "By Title",
+ "statistics": "Statistics",
+ "browse_community": "This Community",
+ "browse_community_by_issue_date": "By Issue Date",
+ "browse_community_by_author": "By Author",
+ "browse_community_by_title": "By Title",
+ "icon": {
+ "pin": "Pin sidebar",
+ "unpin": "Unpin sidebar",
+ "new": "New menu section",
+ "edit": "Edit menu section",
+ "import": "Import menu section",
+ "export": "Export menu section",
+ "access_control": "Access Control menu section",
+ "find": "Find menu section",
+ "registries": "Registries menu section",
+ "curation_task": "Curation Task menu section",
+ "statistics_task": "Statistics Task menu section",
+ "control_panel": "Control Panel menu section"
+ },
+ "toggle": {
+ "new": "Toggle New section",
+ "edit": "Toggle Edit section",
+ "import": "Toggle Import section",
+ "export": "Toggle Export section",
+ "access_control": "Toggle Access Control section",
+ "find": "Toggle Find section",
+ "registries": "Toggle Registries section",
+ "curation_task": "Toggle Curation Task section",
+ "statistics_task": "Toggle Statistics Task section",
+ "control_panel": "Toggle Control Panel section"
+ }
+ }
+ },
"loading": {
"default": "Loading...",
"top-level-communities": "Loading top-level communities...",
"community": "Loading community...",
"collection": "Loading collection...",
"sub-collections": "Loading sub-collections...",
+ "sub-communities": "Loading sub-communities...",
"recent-submissions": "Loading recent submissions...",
"item": "Loading item...",
"objects": "Loading...",
@@ -313,6 +520,7 @@
"community": "Error fetching community",
"collection": "Error fetching collection",
"sub-collections": "Error fetching sub-collections",
+ "sub-communities": "Error fetching sub-communities",
"recent-submissions": "Error fetching recent submissions",
"item": "Error fetching item",
"objects": "Error fetching objects",
@@ -365,5 +573,8 @@
"errors": {
"invalid-user": "Invalid email address or password."
}
+ },
+ "chips": {
+ "remove": "Remove chip"
}
}
diff --git a/resources/images/dspace-logo-mini.svg b/resources/images/dspace-logo-mini.svg
new file mode 100644
index 0000000000..6ca41addc8
--- /dev/null
+++ b/resources/images/dspace-logo-mini.svg
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/resources/images/dspace-logo.png b/resources/images/dspace-logo.png
index 5bb0b36add..40b59feeaa 100644
Binary files a/resources/images/dspace-logo.png and b/resources/images/dspace-logo.png differ
diff --git a/resources/images/dspace-logo.svg b/resources/images/dspace-logo.svg
new file mode 100644
index 0000000000..60df1ed46d
--- /dev/null
+++ b/resources/images/dspace-logo.svg
@@ -0,0 +1,37 @@
+
+
+
+
diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts
index b2cc5129ce..6c5e01b37d 100644
--- a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts
+++ b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts
@@ -7,7 +7,6 @@ import { PaginatedList } from '../../../core/data/paginated-list';
import { MetadataField } from '../../../core/metadata/metadatafield.model';
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
-import { SortOptions } from '../../../core/cache/models/sort-options.model';
@Component({
selector: 'ds-metadata-schema',
diff --git a/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html b/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html
new file mode 100644
index 0000000000..e72a17aac1
--- /dev/null
+++ b/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html
@@ -0,0 +1,11 @@
+
\ No newline at end of file
diff --git a/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.scss b/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.scss
new file mode 100644
index 0000000000..88eb98509a
--- /dev/null
+++ b/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.scss
@@ -0,0 +1 @@
+@import '../../../../styles/variables.scss';
\ No newline at end of file
diff --git a/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts b/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts
new file mode 100644
index 0000000000..30c57c17ea
--- /dev/null
+++ b/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts
@@ -0,0 +1,60 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { MenuService } from '../../../shared/menu/menu.service';
+import { MenuServiceStub } from '../../../shared/testing/menu-service-stub';
+import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
+import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service-stub';
+import { Component } from '@angular/core';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { AdminSidebarSectionComponent } from './admin-sidebar-section.component';
+import { RouterTestingModule } from '@angular/router/testing';
+import { By } from '@angular/platform-browser';
+import { TranslateModule } from '@ngx-translate/core';
+
+describe('AdminSidebarSectionComponent', () => {
+ let component: AdminSidebarSectionComponent;
+ let fixture: ComponentFixture;
+ const menuService = new MenuServiceStub();
+ const iconString = 'test';
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()],
+ declarations: [AdminSidebarSectionComponent, TestComponent],
+ providers: [
+ { provide: 'sectionDataProvider', useValue: { model: { link: 'google.com' }, icon: iconString } },
+ { provide: MenuService, useValue: menuService },
+ { provide: CSSVariableService, useClass: CSSVariableServiceStub },
+ ]
+ }).overrideComponent(AdminSidebarSectionComponent, {
+ set: {
+ entryComponents: [TestComponent]
+ }
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AdminSidebarSectionComponent);
+ component = fixture.componentInstance;
+ spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should set the right icon', () => {
+ const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
+ expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
+ });
+});
+
+// declare a test component
+@Component({
+ selector: 'ds-test-cmp',
+ template: ``
+})
+class TestComponent {
+}
diff --git a/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts
new file mode 100644
index 0000000000..a19a1f95e4
--- /dev/null
+++ b/src/app/+admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts
@@ -0,0 +1,34 @@
+import { Component, Inject, Injector, OnInit } from '@angular/core';
+import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-section.component';
+import { MenuID } from '../../../shared/menu/initial-menus-state';
+import { MenuService } from '../../../shared/menu/menu.service';
+import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
+import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
+import { MenuSection } from '../../../shared/menu/menu.reducer';
+
+/**
+ * Represents a non-expandable section in the admin sidebar
+ */
+@Component({
+ selector: 'ds-admin-sidebar-section',
+ templateUrl: './admin-sidebar-section.component.html',
+ styleUrls: ['./admin-sidebar-section.component.scss'],
+
+})
+@rendersSectionForMenu(MenuID.ADMIN, false)
+export class AdminSidebarSectionComponent extends MenuSectionComponent implements OnInit {
+
+ /**
+ * This section resides in the Admin Sidebar
+ */
+ menuID: MenuID = MenuID.ADMIN;
+ itemModel;
+ constructor(@Inject('sectionDataProvider') menuSection: MenuSection, protected menuService: MenuService, protected injector: Injector,) {
+ super(menuSection, menuService, injector);
+ this.itemModel = menuSection.model as LinkMenuItemModel;
+ }
+
+ ngOnInit(): void {
+ super.ngOnInit();
+ }
+}
diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.html b/src/app/+admin/admin-sidebar/admin-sidebar.component.html
new file mode 100644
index 0000000000..fc9e707bcd
--- /dev/null
+++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.html
@@ -0,0 +1,52 @@
+
\ No newline at end of file
diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.scss b/src/app/+admin/admin-sidebar/admin-sidebar.component.scss
new file mode 100644
index 0000000000..9236cd2a0d
--- /dev/null
+++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.scss
@@ -0,0 +1,77 @@
+@import '../../../styles/variables.scss';
+@import '../../../styles/mixins.scss';
+$icon-z-index: 10;
+
+:host {
+ left: 0;
+ top: 0;
+ height: 100vh;
+ flex: 1 1 auto;
+ nav {
+ height: 100%;
+ flex-direction: column;
+ > div {
+ width: 100%;
+ &.sidebar-top-level-items {
+ flex: 1;
+ overflow: auto;
+ @include dark-scrollbar;
+ }
+ }
+
+ &.inactive ::ng-deep .sidebar-collapsible {
+ margin-left: -#{$sidebar-items-width};
+ }
+
+ .navbar-nav {
+ .admin-menu-header {
+ background-color: $admin-sidebar-header-bg;
+ .logo-wrapper {
+ img {
+ height: 20px;
+ }
+ }
+ .section-header-text {
+ line-height: 1.5;
+ }
+
+ }
+ }
+
+
+ ::ng-deep {
+ .navbar-nav {
+ .sidebar-section {
+ display: flex;
+ align-content: stretch;
+ background-color: $dark;
+ .nav-item {
+ padding-top: $spacer;
+ padding-bottom: $spacer;
+ }
+ .shortcut-icon {
+ padding-left: $icon-padding;
+ padding-right: $icon-padding;
+ }
+ .shortcut-icon, .icon-wrapper {
+ background-color: inherit;
+ z-index: $icon-z-index;
+ }
+ .sidebar-collapsible {
+ width: $sidebar-items-width;
+ position: relative;
+ a {
+ padding-right: $spacer;
+ width: 100%;
+ }
+ }
+ &.active > .sidebar-collapsible > .nav-link {
+ color: $navbar-dark-active-color;
+ }
+ }
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts
new file mode 100644
index 0000000000..c99e8adc58
--- /dev/null
+++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts
@@ -0,0 +1,142 @@
+import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { TranslateModule } from '@ngx-translate/core';
+import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
+import { AdminSidebarComponent } from './admin-sidebar.component';
+import { MenuService } from '../../shared/menu/menu.service';
+import { MenuServiceStub } from '../../shared/testing/menu-service-stub';
+import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
+import { CSSVariableServiceStub } from '../../shared/testing/css-variable-service-stub';
+import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
+import { AuthService } from '../../core/auth/auth.service';
+
+import { of as observableOf } from 'rxjs';
+import { By } from '@angular/platform-browser';
+
+describe('AdminSidebarComponent', () => {
+ let comp: AdminSidebarComponent;
+ let fixture: ComponentFixture;
+ const menuService = new MenuServiceStub();
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [TranslateModule.forRoot(), NoopAnimationsModule],
+ declarations: [AdminSidebarComponent],
+ providers: [
+ { provide: Injector, useValue: {} },
+ { provide: MenuService, useValue: menuService },
+ { provide: CSSVariableService, useClass: CSSVariableServiceStub },
+ { provide: AuthService, useClass: AuthServiceStub }
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ }).overrideComponent(AdminSidebarComponent, {
+ set: {
+ changeDetection: ChangeDetectionStrategy.Default,
+ }
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ spyOn(menuService, 'getMenuTopSections').and.returnValue(observableOf([]));
+ fixture = TestBed.createComponent(AdminSidebarComponent);
+ comp = fixture.componentInstance; // SearchPageComponent test instance
+ comp.sections = observableOf([]);
+ fixture.detectChanges();
+ });
+
+ describe('startSlide', () => {
+ describe('when expanding', () => {
+ beforeEach(() => {
+ comp.sidebarClosed = true;
+ comp.startSlide({ toState: 'expanded' } as any);
+ });
+
+ it('should set the sidebarClosed to false', () => {
+ expect(comp.sidebarClosed).toBeFalsy();
+ })
+ });
+
+ describe('when collapsing', () => {
+ beforeEach(() => {
+ comp.sidebarClosed = false;
+ comp.startSlide({ toState: 'collapsed' } as any);
+ });
+
+ it('should set the sidebarOpen to false', () => {
+ expect(comp.sidebarOpen).toBeFalsy();
+ })
+ })
+ });
+
+ describe('finishSlide', () => {
+ describe('when expanding', () => {
+ beforeEach(() => {
+ comp.sidebarClosed = true;
+ comp.startSlide({ fromState: 'expanded' } as any);
+ });
+
+ it('should set the sidebarClosed to true', () => {
+ expect(comp.sidebarClosed).toBeTruthy();
+ })
+ });
+
+ describe('when collapsing', () => {
+ beforeEach(() => {
+ comp.sidebarClosed = false;
+ comp.startSlide({ fromState: 'collapsed' } as any);
+ });
+
+ it('should set the sidebarOpen to true', () => {
+ expect(comp.sidebarOpen).toBeTruthy();
+ })
+ })
+ });
+
+ describe('when the collapse icon is clicked', () => {
+ beforeEach(() => {
+ spyOn(menuService, 'toggleMenu');
+ const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle')).query(By.css('a.shortcut-icon'));
+ sidebarToggler.triggerEventHandler('click', {preventDefault: () => {/**/}});
+ });
+
+ it('should call toggleMenu on the menuService', () => {
+ expect(menuService.toggleMenu).toHaveBeenCalled();
+ });
+ });
+
+ describe('when the collapse link is clicked', () => {
+ beforeEach(() => {
+ spyOn(menuService, 'toggleMenu');
+ const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle')).query(By.css('.sidebar-collapsible')).query(By.css('a'));
+ sidebarToggler.triggerEventHandler('click', {preventDefault: () => {/**/}});
+ });
+
+ it('should call toggleMenu on the menuService', () => {
+ expect(menuService.toggleMenu).toHaveBeenCalled();
+ });
+ });
+
+ describe('when the the mouse enters the nav tag', () => {
+ it('should call expandPreview on the menuService after 100ms', fakeAsync(() => {
+ spyOn(menuService, 'expandMenuPreview');
+ const sidebarToggler = fixture.debugElement.query(By.css('nav.navbar'));
+ sidebarToggler.triggerEventHandler('mouseenter', {preventDefault: () => {/**/}});
+ tick(99);
+ expect(menuService.expandMenuPreview).not.toHaveBeenCalled();
+ tick(1);
+ expect(menuService.expandMenuPreview).toHaveBeenCalled();
+ }));
+ });
+
+ describe('when the the mouse leaves the nav tag', () => {
+ it('should call collapseMenuPreview on the menuService after 400ms', fakeAsync(() => {
+ spyOn(menuService, 'collapseMenuPreview');
+ const sidebarToggler = fixture.debugElement.query(By.css('nav.navbar'));
+ sidebarToggler.triggerEventHandler('mouseleave', {preventDefault: () => {/**/}});
+ tick(399);
+ expect(menuService.collapseMenuPreview).not.toHaveBeenCalled();
+ tick(1);
+ expect(menuService.collapseMenuPreview).toHaveBeenCalled();
+ }));
+ });
+});
diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts
new file mode 100644
index 0000000000..eb48f64d4d
--- /dev/null
+++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts
@@ -0,0 +1,479 @@
+import { Component, Injector, OnInit } from '@angular/core';
+import { Observable } from 'rxjs/internal/Observable';
+import { slide, slideHorizontal, slideSidebar } from '../../shared/animations/slide';
+import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
+import { MenuService } from '../../shared/menu/menu.service';
+import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state';
+import { MenuComponent } from '../../shared/menu/menu.component';
+import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model';
+import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
+import { AuthService } from '../../core/auth/auth.service';
+import { first, map } from 'rxjs/operators';
+import { combineLatest as combineLatestObservable } from 'rxjs';
+
+/**
+ * Component representing the admin sidebar
+ */
+@Component({
+ selector: 'ds-admin-sidebar',
+ templateUrl: './admin-sidebar.component.html',
+ styleUrls: ['./admin-sidebar.component.scss'],
+ animations: [slideHorizontal, slideSidebar]
+})
+export class AdminSidebarComponent extends MenuComponent implements OnInit {
+ /**
+ * The menu ID of the Navbar is PUBLIC
+ * @type {MenuID.ADMIN}
+ */
+ menuID = MenuID.ADMIN;
+
+ /**
+ * Observable that emits the width of the collapsible menu sections
+ */
+ sidebarWidth: Observable;
+
+ /**
+ * Is true when the sidebar is open, is false when the sidebar is animating or closed
+ * @type {boolean}
+ */
+ sidebarOpen = true; // Open in UI, animation finished
+
+ /**
+ * Is true when the sidebar is closed, is false when the sidebar is animating or open
+ * @type {boolean}
+ */
+ sidebarClosed = !this.sidebarOpen; // Closed in UI, animation finished
+
+ /**
+ * Emits true when either the menu OR the menu's preview is expanded, else emits false
+ */
+ sidebarExpanded: Observable;
+
+ constructor(protected menuService: MenuService,
+ protected injector: Injector,
+ private variableService: CSSVariableService,
+ private authService: AuthService
+ ) {
+ super(menuService, injector);
+ }
+
+ /**
+ * Set and calculate all initial values of the instance variables
+ */
+ ngOnInit(): void {
+ this.createMenu();
+ super.ngOnInit();
+ this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
+ this.authService.isAuthenticated()
+ .subscribe((loggedIn: boolean) => {
+ if (loggedIn) {
+ this.menuService.showMenu(this.menuID);
+ }
+ });
+ this.menuCollapsed.pipe(first())
+ .subscribe((collapsed: boolean) => {
+ this.sidebarOpen = !collapsed;
+ this.sidebarClosed = collapsed;
+ });
+ this.sidebarExpanded = combineLatestObservable(this.menuCollapsed, this.menuPreviewCollapsed)
+ .pipe(
+ map(([collapsed, previewCollapsed]) => (!collapsed || !previewCollapsed))
+ );
+ }
+
+ /**
+ * Initialize all menu sections and items for this menu
+ */
+ private 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.LINK,
+ text: 'menu.section.new_community',
+ link: '/communities/submission'
+ } as LinkMenuItemModel,
+ },
+ {
+ id: 'new_collection',
+ parentID: 'new',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.new_collection',
+ link: '/collections/submission'
+ } as LinkMenuItemModel,
+ },
+ {
+ id: 'new_item',
+ parentID: 'new',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.new_item',
+ link: '/items/submission'
+ } 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,
+ },
+
+ /* 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.LINK,
+ text: 'menu.section.edit_community',
+ link: '#'
+ } as LinkMenuItemModel,
+ },
+ {
+ id: 'edit_collection',
+ parentID: 'edit',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.edit_collection',
+ link: '#'
+ } as LinkMenuItemModel,
+ },
+ {
+ id: 'edit_item',
+ parentID: 'edit',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.edit_item',
+ link: '#'
+ } as LinkMenuItemModel,
+ },
+
+ /* Import */
+ {
+ id: 'import',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.TEXT,
+ text: 'menu.section.import'
+ } as TextMenuItemModel,
+ icon: 'sign-in-alt',
+ index: 2
+ },
+ {
+ id: 'import_metadata',
+ parentID: 'import',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.import_metadata',
+ link: '#'
+ } as LinkMenuItemModel,
+ },
+ {
+ id: 'import_batch',
+ parentID: 'import',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.import_batch',
+ link: '#'
+ } as LinkMenuItemModel,
+ },
+
+ /* Export */
+ {
+ id: 'export',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.TEXT,
+ text: 'menu.section.export'
+ } as TextMenuItemModel,
+ icon: 'sign-out-alt',
+ index: 3
+ },
+ {
+ id: 'export_community',
+ parentID: 'export',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.export_community',
+ link: '#'
+ } as LinkMenuItemModel,
+ },
+ {
+ id: 'export_collection',
+ parentID: 'export',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.export_collection',
+ link: '#'
+ } as LinkMenuItemModel,
+ },
+ {
+ id: 'export_item',
+ parentID: 'export',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.export_item',
+ link: '#'
+ } as LinkMenuItemModel,
+ }, {
+ id: 'export_metadata',
+ parentID: 'export',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.export_metadata',
+ link: '#'
+ } as LinkMenuItemModel,
+ },
+
+ /* Access Control */
+ {
+ id: 'access_control',
+ active: false,
+ visible: true,
+ 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: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.access_control_people',
+ link: '#'
+ } as LinkMenuItemModel,
+ },
+ {
+ id: 'access_control_groups',
+ parentID: 'access_control',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.access_control_groups',
+ link: '#'
+ } as LinkMenuItemModel,
+ },
+ {
+ id: 'access_control_authorizations',
+ parentID: 'access_control',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.access_control_authorizations',
+ link: '#'
+ } as LinkMenuItemModel,
+ },
+
+ /* Search */
+ {
+ id: 'find',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.TEXT,
+ text: 'menu.section.find'
+ } as TextMenuItemModel,
+ icon: 'search',
+ index: 5
+ },
+ {
+ id: 'find_items',
+ parentID: 'find',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.find_items',
+ link: '/search'
+ } as LinkMenuItemModel,
+ },
+ {
+ id: 'find_withdrawn_items',
+ parentID: 'find',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.find_withdrawn_items',
+ link: '#'
+ } as LinkMenuItemModel,
+ },
+ {
+ id: 'find_private_items',
+ parentID: 'find',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.find_private_items',
+ link: '/admin/items'
+ } as LinkMenuItemModel,
+ },
+
+ /* Registries */
+ {
+ id: 'registries',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.TEXT,
+ text: 'menu.section.registries'
+ } as TextMenuItemModel,
+ icon: 'list',
+ index: 6
+ },
+ {
+ id: 'registries_metadata',
+ parentID: 'registries',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.registries_metadata',
+ link: 'admin/registries/metadata'
+ } as LinkMenuItemModel,
+ },
+ {
+ id: 'registries_format',
+ parentID: 'registries',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.registries_format',
+ link: 'admin/registries/bitstream-formats'
+ } as LinkMenuItemModel,
+ },
+
+ /* Curation tasks */
+ {
+ id: 'curation_tasks',
+ active: false,
+ visible: true,
+ model: {
+ type: MenuItemType.LINK,
+ text: 'menu.section.curation_task',
+ link: '/curation'
+ } as LinkMenuItemModel,
+ icon: 'filter',
+ index: 7
+ },
+
+ /* 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
+ },
+
+ /* 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
+ },
+ ];
+ menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
+
+ }
+
+ /**
+ * Method to change this.collapsed to false when the slide animation ends and is sliding open
+ * @param event The animation event
+ */
+ startSlide(event: any): void {
+ if (event.toState === 'expanded') {
+ this.sidebarClosed = false;
+ } else if (event.toState === 'collapsed') {
+ this.sidebarOpen = false;
+ }
+ }
+
+ /**
+ * Method to change this.collapsed to false when the slide animation ends and is sliding open
+ * @param event The animation event
+ */
+ finishSlide(event: any): void {
+ if (event.fromState === 'expanded') {
+ this.sidebarClosed = true;
+ } else if (event.fromState === 'collapsed') {
+ this.sidebarOpen = true;
+ }
+ }
+}
diff --git a/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.html b/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.html
new file mode 100644
index 0000000000..808683910e
--- /dev/null
+++ b/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.html
@@ -0,0 +1,27 @@
+
\ No newline at end of file
diff --git a/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.scss b/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.scss
new file mode 100644
index 0000000000..779cba09d9
--- /dev/null
+++ b/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.scss
@@ -0,0 +1,21 @@
+@import '../../../../styles/variables.scss';
+
+::ng-deep {
+ .fa-chevron-right {
+ padding-left: $spacer/2;
+ font-size: 0.5rem;
+ line-height: 3;
+ }
+
+ .sidebar-sub-level-items {
+ list-style: disc;
+ color: $navbar-dark-color;
+ overflow: hidden;
+
+ }
+
+ .sidebar-collapsible {
+ display: flex;
+ flex-direction: column;
+ }
+}
\ No newline at end of file
diff --git a/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.spec.ts b/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.spec.ts
new file mode 100644
index 0000000000..787386932a
--- /dev/null
+++ b/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.spec.ts
@@ -0,0 +1,84 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ExpandableAdminSidebarSectionComponent } from './expandable-admin-sidebar-section.component';
+import { MenuService } from '../../../shared/menu/menu.service';
+import { MenuServiceStub } from '../../../shared/testing/menu-service-stub';
+import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
+import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service-stub';
+import { of as observableOf } from 'rxjs';
+import { Component } from '@angular/core';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { By } from '@angular/platform-browser';
+import { TranslateModule } from '@ngx-translate/core';
+
+describe('ExpandableAdminSidebarSectionComponent', () => {
+ let component: ExpandableAdminSidebarSectionComponent;
+ let fixture: ComponentFixture;
+ const menuService = new MenuServiceStub();
+ const iconString = 'test';
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [NoopAnimationsModule, TranslateModule.forRoot()],
+ declarations: [ExpandableAdminSidebarSectionComponent, TestComponent],
+ providers: [
+ { provide: 'sectionDataProvider', useValue: {icon: iconString} },
+ { provide: MenuService, useValue: menuService },
+ { provide: CSSVariableService, useClass: CSSVariableServiceStub },
+ ]
+ }).overrideComponent(ExpandableAdminSidebarSectionComponent, {
+ set: {
+ entryComponents: [TestComponent]
+ }
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
+ fixture = TestBed.createComponent(ExpandableAdminSidebarSectionComponent);
+ component = fixture.componentInstance;
+ spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should set the right icon', () => {
+ const icon = fixture.debugElement.query(By.css('.icon-wrapper')).query(By.css('i.fas'));
+ expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
+ });
+
+ describe('when the icon is clicked', () => {
+ beforeEach(() => {
+ spyOn(menuService, 'toggleActiveSection');
+ const sidebarToggler = fixture.debugElement.query(By.css('a.shortcut-icon'));
+ sidebarToggler.triggerEventHandler('click', {preventDefault: () => {/**/}});
+ });
+
+ it('should call toggleActiveSection on the menuService', () => {
+ expect(menuService.toggleActiveSection).toHaveBeenCalled();
+ });
+ });
+
+ describe('when the header text is clicked', () => {
+ beforeEach(() => {
+ spyOn(menuService, 'toggleActiveSection');
+ const sidebarToggler = fixture.debugElement.query(By.css('.sidebar-collapsible')).query(By.css('a'));
+ sidebarToggler.triggerEventHandler('click', {preventDefault: () => {/**/}});
+ });
+
+ it('should call toggleActiveSection on the menuService', () => {
+ expect(menuService.toggleActiveSection).toHaveBeenCalled();
+ });
+ });
+});
+
+// declare a test component
+@Component({
+ selector: 'ds-test-cmp',
+ template: ``
+})
+class TestComponent {
+}
diff --git a/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.ts b/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.ts
new file mode 100644
index 0000000000..4921be77e2
--- /dev/null
+++ b/src/app/+admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.ts
@@ -0,0 +1,69 @@
+import { Component, Inject, Injector, OnInit } from '@angular/core';
+import { rotate } from '../../../shared/animations/rotate';
+import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sidebar-section.component';
+import { slide } from '../../../shared/animations/slide';
+import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
+import { bgColor } from '../../../shared/animations/bgColor';
+import { MenuID } from '../../../shared/menu/initial-menus-state';
+import { MenuService } from '../../../shared/menu/menu.service';
+import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
+
+/**
+ * Represents a expandable section in the sidebar
+ */
+@Component({
+ selector: 'ds-expandable-admin-sidebar-section',
+ templateUrl: './expandable-admin-sidebar-section.component.html',
+ styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
+ animations: [rotate, slide, bgColor]
+
+})
+@rendersSectionForMenu(MenuID.ADMIN, true)
+export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionComponent implements OnInit {
+ /**
+ * This section resides in the Admin Sidebar
+ */
+ menuID = MenuID.ADMIN;
+
+ /**
+ * The background color of the section when it's active
+ */
+ sidebarActiveBg;
+
+ /**
+ * Emits true when the sidebar is currently collapsed, true when it's expanded
+ */
+ sidebarCollapsed: Observable;
+
+ /**
+ * Emits true when the sidebar's preview is currently collapsed, true when it's expanded
+ */
+ sidebarPreviewCollapsed: Observable;
+
+ /**
+ * Emits true when the menu section is expanded, else emits false
+ * This is true when the section is active AND either the sidebar or it's preview is open
+ */
+ expanded: Observable;
+
+ constructor(@Inject('sectionDataProvider') menuSection, protected menuService: MenuService,
+ private variableService: CSSVariableService, protected injector: Injector) {
+ super(menuSection, menuService, injector);
+ }
+
+ /**
+ * Set initial values for instance variables
+ */
+ ngOnInit(): void {
+ super.ngOnInit();
+ this.sidebarActiveBg = this.variableService.getVariable('adminSidebarActiveBg');
+ this.sidebarCollapsed = this.menuService.isMenuCollapsed(this.menuID);
+ this.sidebarPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
+ this.expanded = combineLatestObservable(this.active, this.sidebarCollapsed, this.sidebarPreviewCollapsed)
+ .pipe(
+ map(([active, sidebarCollapsed, sidebarPreviewCollapsed]) => (active && (!sidebarCollapsed || !sidebarPreviewCollapsed)))
+ );
+ }
+}
diff --git a/src/app/+admin/admin.module.ts b/src/app/+admin/admin.module.ts
index b979813376..41d00223ab 100644
--- a/src/app/+admin/admin.module.ts
+++ b/src/app/+admin/admin.module.ts
@@ -5,7 +5,7 @@ import { AdminRoutingModule } from './admin-routing.module';
@NgModule({
imports: [
AdminRegistriesModule,
- AdminRoutingModule
+ AdminRoutingModule,
]
})
export class AdminModule {
diff --git a/src/app/+community-page/community-page.component.html b/src/app/+community-page/community-page.component.html
index 637e37af0c..a86a86c3da 100644
--- a/src/app/+community-page/community-page.component.html
+++ b/src/app/+community-page/community-page.component.html
@@ -24,6 +24,7 @@
[content]="communityPayload.copyrightText"
[hasInnerHtml]="true">
+
diff --git a/src/app/+community-page/community-page.module.ts b/src/app/+community-page/community-page.module.ts
index e00c3910c5..d7f97755c2 100644
--- a/src/app/+community-page/community-page.module.ts
+++ b/src/app/+community-page/community-page.module.ts
@@ -6,6 +6,7 @@ import { SharedModule } from '../shared/shared.module';
import { CommunityPageComponent } from './community-page.component';
import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component';
import { CommunityPageRoutingModule } from './community-page-routing.module';
+import {CommunityPageSubCommunityListComponent} from './sub-community-list/community-page-sub-community-list.component';
@NgModule({
imports: [
@@ -16,6 +17,7 @@ import { CommunityPageRoutingModule } from './community-page-routing.module';
declarations: [
CommunityPageComponent,
CommunityPageSubCollectionListComponent,
+ CommunityPageSubCommunityListComponent,
]
})
export class CommunityPageModule {
diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html
index 12c2578d9c..9156a99b18 100644
--- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html
+++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html
@@ -1,5 +1,5 @@
-
+
0" @fadeIn>
{{'community.sub-collection-list.head' | translate}}
-
diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html
new file mode 100644
index 0000000000..6cd62ba48d
--- /dev/null
+++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html
@@ -0,0 +1,15 @@
+
+
0" @fadeIn>
+
{{'community.sub-community-list.head' | translate}}
+
+
+
+
+
diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.scss b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.scss
new file mode 100644
index 0000000000..50be6f5ad0
--- /dev/null
+++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.scss
@@ -0,0 +1 @@
+@import '../../../styles/variables.scss';
diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts
new file mode 100644
index 0000000000..0c985e37f9
--- /dev/null
+++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts
@@ -0,0 +1,96 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {NO_ERRORS_SCHEMA} from '@angular/core';
+import {CommunityPageSubCommunityListComponent} from './community-page-sub-community-list.component';
+import {Community} from '../../core/shared/community.model';
+import {RemoteData} from '../../core/data/remote-data';
+import {PaginatedList} from '../../core/data/paginated-list';
+import {PageInfo} from '../../core/shared/page-info.model';
+import {SharedModule} from '../../shared/shared.module';
+import {RouterTestingModule} from '@angular/router/testing';
+import {NoopAnimationsModule} from '@angular/platform-browser/animations';
+import {By} from '@angular/platform-browser';
+import {of as observableOf, Observable } from 'rxjs';
+
+describe('SubCommunityList Component', () => {
+ let comp: CommunityPageSubCommunityListComponent;
+ let fixture: ComponentFixture;
+
+ const subcommunities = [Object.assign(new Community(), {
+ name: 'SubCommunity 1',
+ id: '123456789-1',
+ metadata: [
+ {
+ key: 'dc.title',
+ language: 'en_US',
+ value: 'SubCommunity 1'
+ }]
+ }),
+ Object.assign(new Community(), {
+ name: 'SubCommunity 2',
+ id: '123456789-2',
+ metadata: [
+ {
+ key: 'dc.title',
+ language: 'en_US',
+ value: 'SubCommunity 2'
+ }]
+ })
+ ];
+
+ const emptySubCommunitiesCommunity = Object.assign(new Community(), {
+ metadata: [
+ {
+ key: 'dc.title',
+ language: 'en_US',
+ value: 'Test title'
+ }],
+ subcommunities: observableOf(new RemoteData(true, true, true,
+ undefined, new PaginatedList(new PageInfo(), [])))
+ });
+
+ const mockCommunity = Object.assign(new Community(), {
+ metadata: [
+ {
+ key: 'dc.title',
+ language: 'en_US',
+ value: 'Test title'
+ }],
+ subcommunities: observableOf(new RemoteData(true, true, true,
+ undefined, new PaginatedList(new PageInfo(), subcommunities)))
+ })
+ ;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [TranslateModule.forRoot(), SharedModule,
+ RouterTestingModule.withRoutes([]),
+ NoopAnimationsModule],
+ declarations: [CommunityPageSubCommunityListComponent],
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CommunityPageSubCommunityListComponent);
+ comp = fixture.componentInstance;
+ });
+
+ it('should display a list of subCommunities', () => {
+ comp.community = mockCommunity;
+ fixture.detectChanges();
+
+ const subComList = fixture.debugElement.queryAll(By.css('li'));
+ expect(subComList.length).toEqual(2);
+ expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1');
+ expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2');
+ });
+
+ it('should not display the header when subCommunities are empty', () => {
+ comp.community = emptySubCommunitiesCommunity;
+ fixture.detectChanges();
+
+ const subComHead = fixture.debugElement.queryAll(By.css('h2'));
+ expect(subComHead.length).toEqual(0);
+ });
+});
diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts
new file mode 100644
index 0000000000..91f6d7bac1
--- /dev/null
+++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts
@@ -0,0 +1,26 @@
+import { Component, Input, OnInit } from '@angular/core';
+
+import { RemoteData } from '../../core/data/remote-data';
+import { Community } from '../../core/shared/community.model';
+
+import { fadeIn } from '../../shared/animations/fade';
+import { PaginatedList } from '../../core/data/paginated-list';
+import {Observable} from 'rxjs';
+
+@Component({
+ selector: 'ds-community-page-sub-community-list',
+ styleUrls: ['./community-page-sub-community-list.component.scss'],
+ templateUrl: './community-page-sub-community-list.component.html',
+ animations:[fadeIn]
+})
+/**
+ * Component to render the sub-communities of a Community
+ */
+export class CommunityPageSubCommunityListComponent implements OnInit {
+ @Input() community: Community;
+ subCommunitiesRDObs: Observable>>;
+
+ ngOnInit(): void {
+ this.subCommunitiesRDObs = this.community.subcommunities;
+ }
+}
diff --git a/src/app/+home-page/home-news/home-news.component.html b/src/app/+home-page/home-news/home-news.component.html
index ffc88be574..2287329b37 100644
--- a/src/app/+home-page/home-news/home-news.component.html
+++ b/src/app/+home-page/home-news/home-news.component.html
@@ -1,9 +1,7 @@
-
-
-

-
+
+
Welcome to DSpace
DSpace is an open source software platform that enables organisations to:
diff --git a/src/app/+home-page/home-news/home-news.component.scss b/src/app/+home-page/home-news/home-news.component.scss
index 4f4c6df128..3a0cbc7817 100644
--- a/src/app/+home-page/home-news/home-news.component.scss
+++ b/src/app/+home-page/home-news/home-news.component.scss
@@ -6,11 +6,11 @@
margin-bottom: -$content-spacing;
}
-.dspace-logo-container {
- margin: 10px 20px 0px 20px;
+.display-3 {
+ word-break: break-word;
}
-.dspace-logo-container img {
- max-height: 110px;
- max-width: 110px;
+.dspace-logo {
+ height: 110px;
+ width: 110px;
}
diff --git a/src/app/+item-page/edit-item-page/edit-item-operators.spec.ts b/src/app/+item-page/edit-item-page/edit-item-operators.spec.ts
new file mode 100644
index 0000000000..8086a62b8f
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/edit-item-operators.spec.ts
@@ -0,0 +1,35 @@
+import {RemoteData} from '../../core/data/remote-data';
+import {hot} from 'jasmine-marbles';
+import {Item} from '../../core/shared/item.model';
+import {findSuccessfulAccordingTo} from './edit-item-operators';
+
+describe('findSuccessfulAccordingTo', () => {
+ let mockItem1;
+ let mockItem2;
+ let predicate;
+
+ beforeEach(() => {
+ mockItem1 = new Item();
+ mockItem1.isWithdrawn = true;
+
+ mockItem2 = new Item();
+ mockItem1.isWithdrawn = false;
+
+ predicate = (rd: RemoteData
- ) => rd.payload.isWithdrawn;
+ });
+ it('should return first successful RemoteData Observable that complies to predicate', () => {
+ const testRD = {
+ a: new RemoteData(false, false, true, null, undefined),
+ b: new RemoteData(false, false, false, null, mockItem1),
+ c: new RemoteData(false, false, true, null, mockItem2),
+ d: new RemoteData(false, false, true, null, mockItem1),
+ e: new RemoteData(false, false, true, null, mockItem2),
+ };
+
+ const source = hot('abcde', testRD);
+ const result = source.pipe(findSuccessfulAccordingTo(predicate));
+
+ result.subscribe((value) => expect(value).toEqual(testRD.d));
+ });
+
+});
diff --git a/src/app/+item-page/edit-item-page/edit-item-operators.ts b/src/app/+item-page/edit-item-page/edit-item-operators.ts
new file mode 100644
index 0000000000..26c593cac6
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/edit-item-operators.ts
@@ -0,0 +1,13 @@
+import {RemoteData} from '../../core/data/remote-data';
+import {Observable} from 'rxjs';
+import {first} from 'rxjs/operators';
+import {getAllSucceededRemoteData} from '../../core/shared/operators';
+
+/**
+ * Return first Observable of a RemoteData object that complies to the provided predicate
+ * @param predicate
+ */
+export const findSuccessfulAccordingTo = (predicate: (rd: RemoteData) => boolean) =>
+ (source: Observable>): Observable> =>
+ source.pipe(getAllSucceededRemoteData(),
+ first(predicate));
diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.html b/src/app/+item-page/edit-item-page/edit-item-page.component.html
new file mode 100644
index 0000000000..001b484c2c
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/edit-item-page.component.html
@@ -0,0 +1,36 @@
+
+
+
+
{{'item.edit.head' | translate}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/+item-page/edit-item-page/edit-item-page.component.ts b/src/app/+item-page/edit-item-page/edit-item-page.component.ts
new file mode 100644
index 0000000000..b8d3ca7957
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/edit-item-page.component.ts
@@ -0,0 +1,35 @@
+import {fadeIn, fadeInOut} from '../../shared/animations/fade';
+import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {RemoteData} from '../../core/data/remote-data';
+import {Item} from '../../core/shared/item.model';
+import {Observable} from 'rxjs';
+import {map} from 'rxjs/operators';
+
+@Component({
+ selector: 'ds-edit-item-page',
+ templateUrl: './edit-item-page.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ animations: [
+ fadeIn,
+ fadeInOut
+ ]
+})
+/**
+ * Page component for editing an item
+ */
+export class EditItemPageComponent implements OnInit {
+
+ /**
+ * The item to edit
+ */
+ itemRD$: Observable>;
+
+ constructor(private route: ActivatedRoute) {
+ }
+
+ ngOnInit(): void {
+ this.itemRD$ = this.route.data.pipe(map((data) => data.item));
+ }
+
+}
diff --git a/src/app/+item-page/edit-item-page/edit-item-page.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.module.ts
new file mode 100644
index 0000000000..0a7b363d6a
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/edit-item-page.module.ts
@@ -0,0 +1,40 @@
+import {NgModule} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {SharedModule} from '../../shared/shared.module';
+import {EditItemPageRoutingModule} from './edit-item-page.routing.module';
+import {EditItemPageComponent} from './edit-item-page.component';
+import {ItemStatusComponent} from './item-status/item-status.component';
+import {ItemOperationComponent} from './item-operation/item-operation.component';
+import {ModifyItemOverviewComponent} from './modify-item-overview/modify-item-overview.component';
+import {ItemWithdrawComponent} from './item-withdraw/item-withdraw.component';
+import {ItemReinstateComponent} from './item-reinstate/item-reinstate.component';
+import {AbstractSimpleItemActionComponent} from './simple-item-action/abstract-simple-item-action.component';
+import {ItemPrivateComponent} from './item-private/item-private.component';
+import {ItemPublicComponent} from './item-public/item-public.component';
+import {ItemDeleteComponent} from './item-delete/item-delete.component';
+
+/**
+ * Module that contains all components related to the Edit Item page administrator functionality
+ */
+@NgModule({
+ imports: [
+ CommonModule,
+ SharedModule,
+ EditItemPageRoutingModule
+ ],
+ declarations: [
+ EditItemPageComponent,
+ ItemOperationComponent,
+ AbstractSimpleItemActionComponent,
+ ModifyItemOverviewComponent,
+ ItemWithdrawComponent,
+ ItemReinstateComponent,
+ ItemPrivateComponent,
+ ItemPublicComponent,
+ ItemDeleteComponent,
+ ItemStatusComponent
+ ]
+})
+export class EditItemPageModule {
+
+}
diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts
new file mode 100644
index 0000000000..8ef6f43e17
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts
@@ -0,0 +1,72 @@
+import {ItemPageResolver} from '../item-page.resolver';
+import {NgModule} from '@angular/core';
+import {RouterModule} from '@angular/router';
+import {EditItemPageComponent} from './edit-item-page.component';
+import {ItemWithdrawComponent} from './item-withdraw/item-withdraw.component';
+import {ItemReinstateComponent} from './item-reinstate/item-reinstate.component';
+import {ItemPrivateComponent} from './item-private/item-private.component';
+import {ItemPublicComponent} from './item-public/item-public.component';
+import {ItemDeleteComponent} from './item-delete/item-delete.component';
+
+const ITEM_EDIT_WITHDRAW_PATH = 'withdraw';
+const ITEM_EDIT_REINSTATE_PATH = 'reinstate';
+const ITEM_EDIT_PRIVATE_PATH = 'private';
+const ITEM_EDIT_PUBLIC_PATH = 'public';
+const ITEM_EDIT_DELETE_PATH = 'delete';
+
+/**
+ * Routing module that handles the routing for the Edit Item page administrator functionality
+ */
+@NgModule({
+ imports: [
+ RouterModule.forChild([
+ {
+ path: '',
+ component: EditItemPageComponent,
+ resolve: {
+ item: ItemPageResolver
+ }
+ },
+ {
+ path: ITEM_EDIT_WITHDRAW_PATH,
+ component: ItemWithdrawComponent,
+ resolve: {
+ item: ItemPageResolver
+ }
+ },
+ {
+ path: ITEM_EDIT_REINSTATE_PATH,
+ component: ItemReinstateComponent,
+ resolve: {
+ item: ItemPageResolver
+ }
+ },
+ {
+ path: ITEM_EDIT_PRIVATE_PATH,
+ component: ItemPrivateComponent,
+ resolve: {
+ item: ItemPageResolver
+ }
+ },
+ {
+ path: ITEM_EDIT_PUBLIC_PATH,
+ component: ItemPublicComponent,
+ resolve: {
+ item: ItemPageResolver
+ }
+ },
+ {
+ path: ITEM_EDIT_DELETE_PATH,
+ component: ItemDeleteComponent,
+ resolve: {
+ item: ItemPageResolver
+ }
+ }])
+ ],
+ providers: [
+ ItemPageResolver,
+ ]
+})
+export class EditItemPageRoutingModule {
+
+}
diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts
new file mode 100644
index 0000000000..50c494128a
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.spec.ts
@@ -0,0 +1,118 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Item} from '../../../core/shared/item.model';
+import {RouterStub} from '../../../shared/testing/router-stub';
+import {of as observableOf} from 'rxjs';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {RemoteData} from '../../../core/data/remote-data';
+import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
+import {CommonModule} from '@angular/common';
+import {FormsModule} from '@angular/forms';
+import {RouterTestingModule} from '@angular/router/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+import {ActivatedRoute, Router} from '@angular/router';
+import {ItemDataService} from '../../../core/data/item-data.service';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {By} from '@angular/platform-browser';
+import {ItemDeleteComponent} from './item-delete.component';
+import {getItemEditPath} from '../../item-page-routing.module';
+
+let comp: ItemDeleteComponent;
+let fixture: ComponentFixture;
+
+let mockItem;
+let itemPageUrl;
+let routerStub;
+let mockItemDataService: ItemDataService;
+let routeStub;
+let notificationsServiceStub;
+let successfulRestResponse;
+let failRestResponse;
+
+describe('ItemDeleteComponent', () => {
+ beforeEach(async(() => {
+
+ mockItem = Object.assign(new Item(), {
+ id: 'fake-id',
+ handle: 'fake/handle',
+ lastModified: '2018',
+ isWithdrawn: true
+ });
+
+ itemPageUrl = `fake-url/${mockItem.id}`;
+ routerStub = Object.assign(new RouterStub(), {
+ url: `${itemPageUrl}/edit`
+ });
+
+ mockItemDataService = jasmine.createSpyObj('mockItemDataService', {
+ delete: observableOf(new RestResponse(true, '200'))
+ });
+
+ routeStub = {
+ data: observableOf({
+ item: new RemoteData(false, false, true, null, {
+ id: 'fake-id'
+ })
+ })
+ };
+
+ notificationsServiceStub = new NotificationsServiceStub();
+
+ TestBed.configureTestingModule({
+ imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [ItemDeleteComponent],
+ providers: [
+ {provide: ActivatedRoute, useValue: routeStub},
+ {provide: Router, useValue: routerStub},
+ {provide: ItemDataService, useValue: mockItemDataService},
+ {provide: NotificationsService, useValue: notificationsServiceStub},
+ ], schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ successfulRestResponse = new RestResponse(true, '200');
+ failRestResponse = new RestResponse(false, '500');
+
+ fixture = TestBed.createComponent(ItemDeleteComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should render a page with messages based on the \'delete\' messageKey', () => {
+ const header = fixture.debugElement.query(By.css('h2')).nativeElement;
+ expect(header.innerHTML).toContain('item.edit.delete.header');
+ const description = fixture.debugElement.query(By.css('p')).nativeElement;
+ expect(description.innerHTML).toContain('item.edit.delete.description');
+ const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
+ expect(confirmButton.innerHTML).toContain('item.edit.delete.confirm');
+ const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
+ expect(cancelButton.innerHTML).toContain('item.edit.delete.cancel');
+ });
+
+ describe('performAction', () => {
+ it('should call delete function from the ItemDataService', () => {
+ spyOn(comp, 'processRestResponse');
+ comp.performAction();
+
+ expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem.id);
+ expect(comp.processRestResponse).toHaveBeenCalled();
+ });
+ });
+ describe('processRestResponse', () => {
+ it('should navigate to the homepage on successful deletion of the item', () => {
+ comp.processRestResponse(successfulRestResponse);
+ expect(routerStub.navigate).toHaveBeenCalledWith(['']);
+ });
+ });
+ describe('processRestResponse', () => {
+ it('should navigate to the item edit page on failed deletion of the item', () => {
+ comp.processRestResponse(failRestResponse);
+ expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditPath('fake-id')]);
+ });
+ });
+})
+;
diff --git a/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts
new file mode 100644
index 0000000000..68c5738f7d
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-delete/item-delete.component.ts
@@ -0,0 +1,43 @@
+import {Component} from '@angular/core';
+import {first} from 'rxjs/operators';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
+import {getItemEditPath} from '../../item-page-routing.module';
+
+@Component({
+ selector: 'ds-item-delete',
+ templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
+})
+/**
+ * Component responsible for rendering the item delete page
+ */
+export class ItemDeleteComponent extends AbstractSimpleItemActionComponent {
+
+ protected messageKey = 'delete';
+
+ /**
+ * Perform the delete action to the item
+ */
+ performAction() {
+ this.itemDataService.delete(this.item.id).pipe(first()).subscribe(
+ (response: RestResponse) => {
+ this.processRestResponse(response);
+ }
+ );
+ }
+
+ /**
+ * Process the RestResponse retrieved from the server.
+ * When the item is successfully delete, navigate to the homepage, otherwise navigate back to the item edit page
+ * @param response
+ */
+ processRestResponse(response: RestResponse) {
+ if (response.isSuccessful) {
+ this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success'));
+ this.router.navigate(['']);
+ } else {
+ this.notificationsService.error(this.translateService.get('item.edit.' + this.messageKey + '.error'));
+ this.router.navigate([getItemEditPath(this.item.id)]);
+ }
+ }
+}
diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html
new file mode 100644
index 0000000000..4623195437
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html
@@ -0,0 +1,15 @@
+
+
+ {{'item.edit.tabs.status.buttons.' + operation.operationKey + '.label' | translate}}
+
+
+
+
+
+ {{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
+
+
\ No newline at end of file
diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts
new file mode 100644
index 0000000000..1901bf5fb4
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts
@@ -0,0 +1,45 @@
+import {ItemOperation} from './itemOperation.model';
+import {async, TestBed} from '@angular/core/testing';
+import {ItemOperationComponent} from './item-operation.component';
+import {TranslateModule} from '@ngx-translate/core';
+import {By} from '@angular/platform-browser';
+
+describe('ItemOperationComponent', () => {
+ let itemOperation: ItemOperation;
+
+ let fixture;
+ let comp;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [TranslateModule.forRoot()],
+ declarations: [ItemOperationComponent]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ itemOperation = new ItemOperation('key1', 'url1');
+
+ fixture = TestBed.createComponent(ItemOperationComponent);
+ comp = fixture.componentInstance;
+ comp.operation = itemOperation;
+ fixture.detectChanges();
+ });
+
+ it('should render operation row', () => {
+ const span = fixture.debugElement.query(By.css('span')).nativeElement;
+ expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label');
+ const link = fixture.debugElement.query(By.css('a')).nativeElement;
+ expect(link.href).toContain('url1');
+ expect(link.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
+ });
+ it('should render disabled operation row', () => {
+ itemOperation.setDisabled(true);
+ fixture.detectChanges();
+
+ const span = fixture.debugElement.query(By.css('span')).nativeElement;
+ expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label');
+ const span2 = fixture.debugElement.query(By.css('span.btn-danger')).nativeElement;
+ expect(span2.textContent).toContain('item.edit.tabs.status.buttons.key1.button');
+ });
+});
diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts
new file mode 100644
index 0000000000..76d056df95
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.ts
@@ -0,0 +1,15 @@
+import {Component, Input} from '@angular/core';
+import {ItemOperation} from './itemOperation.model';
+
+@Component({
+ selector: 'ds-item-operation',
+ templateUrl: './item-operation.component.html'
+})
+/**
+ * Operation that can be performed on an item
+ */
+export class ItemOperationComponent {
+
+ @Input() operation: ItemOperation;
+
+}
diff --git a/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts b/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts
new file mode 100644
index 0000000000..105889d42d
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts
@@ -0,0 +1,25 @@
+/**
+ * Represents an item operation used on the edit item page with a key, an operation URL to which will be navigated
+ * when performing the action and an option to disable the operation.
+ */
+export class ItemOperation {
+
+ operationKey: string;
+ operationUrl: string;
+ disabled: boolean;
+
+ constructor(operationKey: string, operationUrl: string) {
+ this.operationKey = operationKey;
+ this.operationUrl = operationUrl;
+ this.setDisabled(false);
+ }
+
+ /**
+ * Set whether this operation should be disabled
+ * @param disabled
+ */
+ setDisabled(disabled: boolean): void {
+ this.disabled = disabled;
+ }
+
+}
diff --git a/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts b/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts
new file mode 100644
index 0000000000..5b99ced743
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-private/item-private.component.spec.ts
@@ -0,0 +1,105 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Item} from '../../../core/shared/item.model';
+import {RouterStub} from '../../../shared/testing/router-stub';
+import {of as observableOf} from 'rxjs';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {RemoteData} from '../../../core/data/remote-data';
+import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
+import {CommonModule} from '@angular/common';
+import {FormsModule} from '@angular/forms';
+import {RouterTestingModule} from '@angular/router/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+import {ActivatedRoute, Router} from '@angular/router';
+import {ItemDataService} from '../../../core/data/item-data.service';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {By} from '@angular/platform-browser';
+import {ItemPrivateComponent} from './item-private.component';
+
+let comp: ItemPrivateComponent;
+let fixture: ComponentFixture;
+
+let mockItem;
+let itemPageUrl;
+let routerStub;
+let mockItemDataService: ItemDataService;
+let routeStub;
+let notificationsServiceStub;
+let successfulRestResponse;
+let failRestResponse;
+
+describe('ItemPrivateComponent', () => {
+ beforeEach(async(() => {
+
+ mockItem = Object.assign(new Item(), {
+ id: 'fake-id',
+ handle: 'fake/handle',
+ lastModified: '2018',
+ isWithdrawn: true
+ });
+
+ itemPageUrl = `fake-url/${mockItem.id}`;
+ routerStub = Object.assign(new RouterStub(), {
+ url: `${itemPageUrl}/edit`
+ });
+
+ mockItemDataService = jasmine.createSpyObj('mockItemDataService',{
+ setDiscoverable: observableOf(new RestResponse(true, '200'))
+ });
+
+ routeStub = {
+ data: observableOf({
+ item: new RemoteData(false, false, true, null, {
+ id: 'fake-id'
+ })
+ })
+ };
+
+ notificationsServiceStub = new NotificationsServiceStub();
+
+ TestBed.configureTestingModule({
+ imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [ItemPrivateComponent],
+ providers: [
+ {provide: ActivatedRoute, useValue: routeStub},
+ {provide: Router, useValue: routerStub},
+ {provide: ItemDataService, useValue: mockItemDataService},
+ {provide: NotificationsService, useValue: notificationsServiceStub},
+ ], schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ successfulRestResponse = new RestResponse(true, '200');
+ failRestResponse = new RestResponse(false, '500');
+
+ fixture = TestBed.createComponent(ItemPrivateComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should render a page with messages based on the \'private\' messageKey', () => {
+ const header = fixture.debugElement.query(By.css('h2')).nativeElement;
+ expect(header.innerHTML).toContain('item.edit.private.header');
+ const description = fixture.debugElement.query(By.css('p')).nativeElement;
+ expect(description.innerHTML).toContain('item.edit.private.description');
+ const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
+ expect(confirmButton.innerHTML).toContain('item.edit.private.confirm');
+ const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
+ expect(cancelButton.innerHTML).toContain('item.edit.private.cancel');
+ });
+
+ describe('performAction', () => {
+ it('should call setDiscoverable function from the ItemDataService', () => {
+ spyOn(comp, 'processRestResponse');
+ comp.performAction();
+
+ expect(mockItemDataService.setDiscoverable).toHaveBeenCalledWith(mockItem.id, false);
+ expect(comp.processRestResponse).toHaveBeenCalled();
+ });
+ });
+})
+;
diff --git a/src/app/+item-page/edit-item-page/item-private/item-private.component.ts b/src/app/+item-page/edit-item-page/item-private/item-private.component.ts
new file mode 100644
index 0000000000..f1e7600c18
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-private/item-private.component.ts
@@ -0,0 +1,30 @@
+import {Component} from '@angular/core';
+import {first} from 'rxjs/operators';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
+import {RemoteData} from '../../../core/data/remote-data';
+import {Item} from '../../../core/shared/item.model';
+
+@Component({
+ selector: 'ds-item-private',
+ templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
+})
+/**
+ * Component responsible for rendering the make item private page
+ */
+export class ItemPrivateComponent extends AbstractSimpleItemActionComponent {
+
+ protected messageKey = 'private';
+ protected predicate = (rd: RemoteData- ) => !rd.payload.isDiscoverable;
+
+ /**
+ * Perform the make private action to the item
+ */
+ performAction() {
+ this.itemDataService.setDiscoverable(this.item.id, false).pipe(first()).subscribe(
+ (response: RestResponse) => {
+ this.processRestResponse(response);
+ }
+ );
+ }
+}
diff --git a/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts b/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts
new file mode 100644
index 0000000000..182d3ffabe
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-public/item-public.component.spec.ts
@@ -0,0 +1,105 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Item} from '../../../core/shared/item.model';
+import {RouterStub} from '../../../shared/testing/router-stub';
+import {of as observableOf} from 'rxjs';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {RemoteData} from '../../../core/data/remote-data';
+import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
+import {CommonModule} from '@angular/common';
+import {FormsModule} from '@angular/forms';
+import {RouterTestingModule} from '@angular/router/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+import {ActivatedRoute, Router} from '@angular/router';
+import {ItemDataService} from '../../../core/data/item-data.service';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {By} from '@angular/platform-browser';
+import {ItemPublicComponent} from './item-public.component';
+
+let comp: ItemPublicComponent;
+let fixture: ComponentFixture;
+
+let mockItem;
+let itemPageUrl;
+let routerStub;
+let mockItemDataService: ItemDataService;
+let routeStub;
+let notificationsServiceStub;
+let successfulRestResponse;
+let failRestResponse;
+
+describe('ItemPublicComponent', () => {
+ beforeEach(async(() => {
+
+ mockItem = Object.assign(new Item(), {
+ id: 'fake-id',
+ handle: 'fake/handle',
+ lastModified: '2018',
+ isWithdrawn: true
+ });
+
+ itemPageUrl = `fake-url/${mockItem.id}`;
+ routerStub = Object.assign(new RouterStub(), {
+ url: `${itemPageUrl}/edit`
+ });
+
+ mockItemDataService = jasmine.createSpyObj('mockItemDataService',{
+ setDiscoverable: observableOf(new RestResponse(true, '200'))
+ });
+
+ routeStub = {
+ data: observableOf({
+ item: new RemoteData(false, false, true, null, {
+ id: 'fake-id'
+ })
+ })
+ };
+
+ notificationsServiceStub = new NotificationsServiceStub();
+
+ TestBed.configureTestingModule({
+ imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [ItemPublicComponent],
+ providers: [
+ {provide: ActivatedRoute, useValue: routeStub},
+ {provide: Router, useValue: routerStub},
+ {provide: ItemDataService, useValue: mockItemDataService},
+ {provide: NotificationsService, useValue: notificationsServiceStub},
+ ], schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ successfulRestResponse = new RestResponse(true, '200');
+ failRestResponse = new RestResponse(false, '500');
+
+ fixture = TestBed.createComponent(ItemPublicComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should render a page with messages based on the \'public\' messageKey', () => {
+ const header = fixture.debugElement.query(By.css('h2')).nativeElement;
+ expect(header.innerHTML).toContain('item.edit.public.header');
+ const description = fixture.debugElement.query(By.css('p')).nativeElement;
+ expect(description.innerHTML).toContain('item.edit.public.description');
+ const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
+ expect(confirmButton.innerHTML).toContain('item.edit.public.confirm');
+ const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
+ expect(cancelButton.innerHTML).toContain('item.edit.public.cancel');
+ });
+
+ describe('performAction', () => {
+ it('should call setDiscoverable function from the ItemDataService', () => {
+ spyOn(comp, 'processRestResponse');
+ comp.performAction();
+
+ expect(mockItemDataService.setDiscoverable).toHaveBeenCalledWith(mockItem.id, true);
+ expect(comp.processRestResponse).toHaveBeenCalled();
+ });
+ });
+})
+;
diff --git a/src/app/+item-page/edit-item-page/item-public/item-public.component.ts b/src/app/+item-page/edit-item-page/item-public/item-public.component.ts
new file mode 100644
index 0000000000..d65d0f171d
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-public/item-public.component.ts
@@ -0,0 +1,30 @@
+import {Component} from '@angular/core';
+import {first} from 'rxjs/operators';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
+import {RemoteData} from '../../../core/data/remote-data';
+import {Item} from '../../../core/shared/item.model';
+
+@Component({
+ selector: 'ds-item-public',
+ templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
+})
+/**
+ * Component responsible for rendering the make item public page
+ */
+export class ItemPublicComponent extends AbstractSimpleItemActionComponent {
+
+ protected messageKey = 'public';
+ protected predicate = (rd: RemoteData
- ) => rd.payload.isDiscoverable;
+
+ /**
+ * Perform the make public action to the item
+ */
+ performAction() {
+ this.itemDataService.setDiscoverable(this.item.id, true).pipe(first()).subscribe(
+ (response: RestResponse) => {
+ this.processRestResponse(response);
+ }
+ );
+ }
+}
diff --git a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts
new file mode 100644
index 0000000000..dbea7f3e69
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.spec.ts
@@ -0,0 +1,105 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Item} from '../../../core/shared/item.model';
+import {RouterStub} from '../../../shared/testing/router-stub';
+import {of as observableOf} from 'rxjs';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {RemoteData} from '../../../core/data/remote-data';
+import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
+import {CommonModule} from '@angular/common';
+import {FormsModule} from '@angular/forms';
+import {RouterTestingModule} from '@angular/router/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+import {ActivatedRoute, Router} from '@angular/router';
+import {ItemDataService} from '../../../core/data/item-data.service';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {By} from '@angular/platform-browser';
+import {ItemReinstateComponent} from './item-reinstate.component';
+
+let comp: ItemReinstateComponent;
+let fixture: ComponentFixture;
+
+let mockItem;
+let itemPageUrl;
+let routerStub;
+let mockItemDataService: ItemDataService;
+let routeStub;
+let notificationsServiceStub;
+let successfulRestResponse;
+let failRestResponse;
+
+describe('ItemReinstateComponent', () => {
+ beforeEach(async(() => {
+
+ mockItem = Object.assign(new Item(), {
+ id: 'fake-id',
+ handle: 'fake/handle',
+ lastModified: '2018',
+ isWithdrawn: true
+ });
+
+ itemPageUrl = `fake-url/${mockItem.id}`;
+ routerStub = Object.assign(new RouterStub(), {
+ url: `${itemPageUrl}/edit`
+ });
+
+ mockItemDataService = jasmine.createSpyObj('mockItemDataService',{
+ setWithDrawn: observableOf(new RestResponse(true, '200'))
+ });
+
+ routeStub = {
+ data: observableOf({
+ item: new RemoteData(false, false, true, null, {
+ id: 'fake-id'
+ })
+ })
+ };
+
+ notificationsServiceStub = new NotificationsServiceStub();
+
+ TestBed.configureTestingModule({
+ imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [ItemReinstateComponent],
+ providers: [
+ {provide: ActivatedRoute, useValue: routeStub},
+ {provide: Router, useValue: routerStub},
+ {provide: ItemDataService, useValue: mockItemDataService},
+ {provide: NotificationsService, useValue: notificationsServiceStub},
+ ], schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ successfulRestResponse = new RestResponse(true, '200');
+ failRestResponse = new RestResponse(false, '500');
+
+ fixture = TestBed.createComponent(ItemReinstateComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should render a page with messages based on the \'reinstate\' messageKey', () => {
+ const header = fixture.debugElement.query(By.css('h2')).nativeElement;
+ expect(header.innerHTML).toContain('item.edit.reinstate.header');
+ const description = fixture.debugElement.query(By.css('p')).nativeElement;
+ expect(description.innerHTML).toContain('item.edit.reinstate.description');
+ const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
+ expect(confirmButton.innerHTML).toContain('item.edit.reinstate.confirm');
+ const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
+ expect(cancelButton.innerHTML).toContain('item.edit.reinstate.cancel');
+ });
+
+ describe('performAction', () => {
+ it('should call setWithdrawn function from the ItemDataService', () => {
+ spyOn(comp, 'processRestResponse');
+ comp.performAction();
+
+ expect(mockItemDataService.setWithDrawn).toHaveBeenCalledWith(mockItem.id, false);
+ expect(comp.processRestResponse).toHaveBeenCalled();
+ });
+ });
+})
+;
diff --git a/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts
new file mode 100644
index 0000000000..5c710b0a81
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-reinstate/item-reinstate.component.ts
@@ -0,0 +1,30 @@
+import {Component} from '@angular/core';
+import {first} from 'rxjs/operators';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
+import {RemoteData} from '../../../core/data/remote-data';
+import {Item} from '../../../core/shared/item.model';
+
+@Component({
+ selector: 'ds-item-reinstate',
+ templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
+})
+/**
+ * Component responsible for rendering the Item Reinstate page
+ */
+export class ItemReinstateComponent extends AbstractSimpleItemActionComponent {
+
+ protected messageKey = 'reinstate';
+ protected predicate = (rd: RemoteData
- ) => !rd.payload.isWithdrawn;
+
+ /**
+ * Perform the reinstate action to the item
+ */
+ performAction() {
+ this.itemDataService.setWithDrawn(this.item.id, false).pipe(first()).subscribe(
+ (response: RestResponse) => {
+ this.processRestResponse(response);
+ }
+ );
+ }
+}
diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.html b/src/app/+item-page/edit-item-page/item-status/item-status.component.html
new file mode 100644
index 0000000000..0f7d9a5607
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.html
@@ -0,0 +1,21 @@
+
{{'item.edit.tabs.status.description' | translate}}
+
+
+
+ {{'item.edit.tabs.status.labels.' + statusKey | translate}}:
+
+
+ {{statusData[statusKey]}}
+
+
+
+ {{'item.edit.tabs.status.labels.itemPage' | translate}}:
+
+
+
+
+
+
+
diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts
new file mode 100644
index 0000000000..319d4c47ae
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.spec.ts
@@ -0,0 +1,68 @@
+import { ItemStatusComponent } from './item-status.component';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { TranslateModule } from '@ngx-translate/core';
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
+import { CommonModule } from '@angular/common';
+import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
+import { HostWindowService } from '../../../shared/host-window.service';
+import { RouterTestingModule } from '@angular/router/testing';
+import { Router } from '@angular/router';
+import { RouterStub } from '../../../shared/testing/router-stub';
+import { Item } from '../../../core/shared/item.model';
+import { By } from '@angular/platform-browser';
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+
+describe('ItemStatusComponent', () => {
+ let comp: ItemStatusComponent;
+ let fixture: ComponentFixture;
+
+ const mockItem = Object.assign(new Item(), {
+ id: 'fake-id',
+ handle: 'fake/handle',
+ lastModified: '2018'
+ });
+
+ const itemPageUrl = `fake-url/${mockItem.id}`;
+ const routerStub = Object.assign(new RouterStub(), {
+ url: `${itemPageUrl}/edit`
+ });
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [ItemStatusComponent],
+ providers: [
+ { provide: Router, useValue: routerStub },
+ { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
+ ], schemas: [CUSTOM_ELEMENTS_SCHEMA]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ItemStatusComponent);
+ comp = fixture.componentInstance;
+ comp.item = mockItem;
+ fixture.detectChanges();
+ });
+
+ it('should display the item\'s internal id', () => {
+ const statusId: HTMLElement = fixture.debugElement.query(By.css('.status-data#status-id')).nativeElement;
+ expect(statusId.textContent).toContain(mockItem.id);
+ });
+
+ it('should display the item\'s handle', () => {
+ const statusHandle: HTMLElement = fixture.debugElement.query(By.css('.status-data#status-handle')).nativeElement;
+ expect(statusHandle.textContent).toContain(mockItem.handle);
+ });
+
+ it('should display the item\'s last modified date', () => {
+ const statusLastModified: HTMLElement = fixture.debugElement.query(By.css('.status-data#status-lastModified')).nativeElement;
+ expect(statusLastModified.textContent).toContain(mockItem.lastModified);
+ });
+
+ it('should display the item\'s page url', () => {
+ const statusItemPage: HTMLElement = fixture.debugElement.query(By.css('.status-data#status-itemPage')).nativeElement;
+ expect(statusItemPage.textContent).toContain(itemPageUrl);
+ });
+
+});
diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts
new file mode 100644
index 0000000000..2b2c7a2ed4
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts
@@ -0,0 +1,95 @@
+import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core';
+import {fadeIn, fadeInOut} from '../../../shared/animations/fade';
+import {Item} from '../../../core/shared/item.model';
+import {Router} from '@angular/router';
+import {ItemOperation} from '../item-operation/itemOperation.model';
+
+@Component({
+ selector: 'ds-item-status',
+ templateUrl: './item-status.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ animations: [
+ fadeIn,
+ fadeInOut
+ ]
+})
+/**
+ * Component for displaying an item's status
+ */
+export class ItemStatusComponent implements OnInit {
+
+ /**
+ * The item to display the status for
+ */
+ @Input() item: Item;
+
+ /**
+ * The data to show in the status
+ */
+ statusData: any;
+ /**
+ * The keys of the data (to loop over)
+ */
+ statusDataKeys;
+
+ /**
+ * The possible actions that can be performed on the item
+ * key: id value: url to action's component
+ */
+ operations: ItemOperation[];
+ /**
+ * The keys of the actions (to loop over)
+ */
+ actionsKeys;
+
+ constructor(private router: Router) {
+ }
+
+ ngOnInit(): void {
+ this.statusData = Object.assign({
+ id: this.item.id,
+ handle: this.item.handle,
+ lastModified: this.item.lastModified
+ });
+ this.statusDataKeys = Object.keys(this.statusData);
+
+ /*
+ The key is used to build messages
+ i18n example: 'item.edit.tabs.status.buttons..label'
+ The value is supposed to be a href for the button
+ */
+ this.operations = [];
+ if (this.item.isWithdrawn) {
+ this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl() + '/reinstate'));
+ } else {
+ this.operations.push(new ItemOperation('withdraw', this.getCurrentUrl() + '/withdraw'));
+ }
+ if (this.item.isDiscoverable) {
+ this.operations.push(new ItemOperation('private', this.getCurrentUrl() + '/private'));
+ } else {
+ this.operations.push(new ItemOperation('public', this.getCurrentUrl() + '/public'));
+ }
+ this.operations.push(new ItemOperation('delete', this.getCurrentUrl() + '/delete'));
+ }
+
+ /**
+ * Get the url to the simple item page
+ * @returns {string} url
+ */
+ getItemPage(): string {
+ return this.router.url.substr(0, this.router.url.lastIndexOf('/'));
+ }
+
+ /**
+ * Get the current url without query params
+ * @returns {string} url
+ */
+ getCurrentUrl(): string {
+ if (this.router.url.indexOf('?') > -1) {
+ return this.router.url.substr(0, this.router.url.indexOf('?'));
+ } else {
+ return this.router.url;
+ }
+ }
+
+}
diff --git a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts
new file mode 100644
index 0000000000..e1de52a506
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.spec.ts
@@ -0,0 +1,105 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Item} from '../../../core/shared/item.model';
+import {RouterStub} from '../../../shared/testing/router-stub';
+import {of as observableOf} from 'rxjs';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {RemoteData} from '../../../core/data/remote-data';
+import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
+import {CommonModule} from '@angular/common';
+import {FormsModule} from '@angular/forms';
+import {RouterTestingModule} from '@angular/router/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+import {ActivatedRoute, Router} from '@angular/router';
+import {ItemDataService} from '../../../core/data/item-data.service';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {ItemWithdrawComponent} from './item-withdraw.component';
+import {By} from '@angular/platform-browser';
+
+let comp: ItemWithdrawComponent;
+let fixture: ComponentFixture;
+
+let mockItem;
+let itemPageUrl;
+let routerStub;
+let mockItemDataService: ItemDataService;
+let routeStub;
+let notificationsServiceStub;
+let successfulRestResponse;
+let failRestResponse;
+
+describe('ItemWithdrawComponent', () => {
+ beforeEach(async(() => {
+
+ mockItem = Object.assign(new Item(), {
+ id: 'fake-id',
+ handle: 'fake/handle',
+ lastModified: '2018',
+ isWithdrawn: true
+ });
+
+ itemPageUrl = `fake-url/${mockItem.id}`;
+ routerStub = Object.assign(new RouterStub(), {
+ url: `${itemPageUrl}/edit`
+ });
+
+ mockItemDataService = jasmine.createSpyObj('mockItemDataService',{
+ setWithDrawn: observableOf(new RestResponse(true, '200'))
+ });
+
+ routeStub = {
+ data: observableOf({
+ item: new RemoteData(false, false, true, null, {
+ id: 'fake-id'
+ })
+ })
+ };
+
+ notificationsServiceStub = new NotificationsServiceStub();
+
+ TestBed.configureTestingModule({
+ imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot(),],
+ declarations: [ItemWithdrawComponent],
+ providers: [
+ {provide: ActivatedRoute, useValue: routeStub},
+ {provide: Router, useValue: routerStub},
+ {provide: ItemDataService, useValue: mockItemDataService},
+ {provide: NotificationsService, useValue: notificationsServiceStub},
+ ], schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ successfulRestResponse = new RestResponse(true, '200');
+ failRestResponse = new RestResponse(false, '500');
+
+ fixture = TestBed.createComponent(ItemWithdrawComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should render a page with messages based on the \'withdraw\' messageKey', () => {
+ const header = fixture.debugElement.query(By.css('h2')).nativeElement;
+ expect(header.innerHTML).toContain('item.edit.withdraw.header');
+ const description = fixture.debugElement.query(By.css('p')).nativeElement;
+ expect(description.innerHTML).toContain('item.edit.withdraw.description');
+ const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
+ expect(confirmButton.innerHTML).toContain('item.edit.withdraw.confirm');
+ const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
+ expect(cancelButton.innerHTML).toContain('item.edit.withdraw.cancel');
+ });
+
+ describe('performAction', () => {
+ it('should call setWithdrawn function from the ItemDataService', () => {
+ spyOn(comp, 'processRestResponse');
+ comp.performAction();
+
+ expect(mockItemDataService.setWithDrawn).toHaveBeenCalledWith(mockItem.id, true);
+ expect(comp.processRestResponse).toHaveBeenCalled();
+ });
+ });
+})
+;
diff --git a/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts
new file mode 100644
index 0000000000..6924124efc
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/item-withdraw/item-withdraw.component.ts
@@ -0,0 +1,30 @@
+import {Component} from '@angular/core';
+import {first} from 'rxjs/operators';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
+import {RemoteData} from '../../../core/data/remote-data';
+import {Item} from '../../../core/shared/item.model';
+
+@Component({
+ selector: 'ds-item-withdraw',
+ templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
+})
+/**
+ * Component responsible for rendering the Item Withdraw page
+ */
+export class ItemWithdrawComponent extends AbstractSimpleItemActionComponent {
+
+ protected messageKey = 'withdraw';
+ protected predicate = (rd: RemoteData- ) => rd.payload.isWithdrawn;
+
+ /**
+ * Perform the withdraw action to the item
+ */
+ performAction() {
+ this.itemDataService.setWithDrawn(this.item.id, true).pipe(first()).subscribe(
+ (response: RestResponse) => {
+ this.processRestResponse(response);
+ }
+ );
+ }
+}
diff --git a/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.html b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.html
new file mode 100644
index 0000000000..d59d29ddbf
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.html
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.spec.ts b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.spec.ts
new file mode 100644
index 0000000000..942357dc5a
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.spec.ts
@@ -0,0 +1,55 @@
+import {Item} from '../../../core/shared/item.model';
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {ModifyItemOverviewComponent} from './modify-item-overview.component';
+import {By} from '@angular/platform-browser';
+import {TranslateModule} from '@ngx-translate/core';
+
+let comp: ModifyItemOverviewComponent;
+let fixture: ComponentFixture;
+
+const mockItem = Object.assign(new Item(), {
+ id: 'fake-id',
+ handle: 'fake/handle',
+ lastModified: '2018',
+ metadata: [
+ {key: 'dc.title', value: 'Mock item title', language: 'en'},
+ {key: 'dc.contributor.author', value: 'Mayer, Ed', language: ''}
+ ]
+});
+
+describe('ModifyItemOverviewComponent', () => {
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [TranslateModule.forRoot()],
+ declarations: [ModifyItemOverviewComponent],
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ModifyItemOverviewComponent);
+ comp = fixture.componentInstance;
+ comp.item = mockItem;
+
+ fixture.detectChanges();
+ });
+ it('should render a table of existing metadata fields in the item', () => {
+
+ const metadataRows = fixture.debugElement.queryAll(By.css('tr.metadata-row'));
+ expect(metadataRows.length).toEqual(2);
+
+ const titleRow = metadataRows[0].queryAll(By.css('td'));
+ expect(titleRow.length).toEqual(3);
+
+ expect(titleRow[0].nativeElement.innerHTML).toContain('dc.title');
+ expect(titleRow[1].nativeElement.innerHTML).toContain('Mock item title');
+ expect(titleRow[2].nativeElement.innerHTML).toContain('en');
+
+ const authorRow = metadataRows[1].queryAll(By.css('td'));
+ expect(authorRow.length).toEqual(3);
+
+ expect(authorRow[0].nativeElement.innerHTML).toContain('dc.contributor.author');
+ expect(authorRow[1].nativeElement.innerHTML).toContain('Mayer, Ed');
+ expect(authorRow[2].nativeElement.innerHTML).toEqual('');
+
+ });
+});
diff --git a/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts
new file mode 100644
index 0000000000..d32a98d5e0
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts
@@ -0,0 +1,20 @@
+import {Component, Input, OnInit} from '@angular/core';
+import {Item} from '../../../core/shared/item.model';
+import {Metadatum} from '../../../core/shared/metadatum.model';
+
+@Component({
+ selector: 'ds-modify-item-overview',
+ templateUrl: './modify-item-overview.component.html'
+})
+/**
+ * Component responsible for rendering a table containing the metadatavalues from the to be edited item
+ */
+export class ModifyItemOverviewComponent implements OnInit {
+
+ @Input() item: Item;
+ metadata: Metadatum[];
+
+ ngOnInit(): void {
+ this.metadata = this.item.metadata;
+ }
+}
diff --git a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.html b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.html
new file mode 100644
index 0000000000..fef76231c6
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.html
@@ -0,0 +1,16 @@
+
+
+
+
{{headerMessage | translate: {id: item.handle} }}
+
{{descriptionMessage | translate}}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts
new file mode 100644
index 0000000000..2da671517c
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.spec.ts
@@ -0,0 +1,142 @@
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {Item} from '../../../core/shared/item.model';
+import {RouterStub} from '../../../shared/testing/router-stub';
+import {CommonModule} from '@angular/common';
+import {RouterTestingModule} from '@angular/router/testing';
+import {TranslateModule} from '@ngx-translate/core';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+import {ActivatedRoute, Router} from '@angular/router';
+import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
+import {FormsModule} from '@angular/forms';
+import {ItemDataService} from '../../../core/data/item-data.service';
+import {RemoteData} from '../../../core/data/remote-data';
+import {AbstractSimpleItemActionComponent} from './abstract-simple-item-action.component';
+import {By} from '@angular/platform-browser';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {of as observableOf} from 'rxjs';
+import {getItemEditPath} from '../../item-page-routing.module';
+
+/**
+ * Test component that implements the AbstractSimpleItemActionComponent used to test the
+ * AbstractSimpleItemActionComponent component
+ */
+@Component({
+ selector: 'ds-simple-action',
+ templateUrl: './abstract-simple-item-action.component.html'
+})
+export class MySimpleItemActionComponent extends AbstractSimpleItemActionComponent {
+
+ protected messageKey = 'myEditAction';
+ protected predicate = (rd: RemoteData- ) => rd.payload.isWithdrawn;
+
+ performAction() {
+ // do nothing
+ }
+
+}
+
+let comp: MySimpleItemActionComponent;
+let fixture: ComponentFixture;
+
+let mockItem;
+let itemPageUrl;
+let routerStub;
+let mockItemDataService;
+let routeStub;
+let notificationsServiceStub;
+let successfulRestResponse;
+let failRestResponse;
+
+describe('AbstractSimpleItemActionComponent', () => {
+ beforeEach(async(() => {
+
+ mockItem = Object.assign(new Item(), {
+ id: 'fake-id',
+ handle: 'fake/handle',
+ lastModified: '2018',
+ isWithdrawn: true
+ });
+
+ itemPageUrl = `fake-url/${mockItem.id}`;
+ routerStub = Object.assign(new RouterStub(), {
+ url: `${itemPageUrl}/edit`
+ });
+
+ mockItemDataService = jasmine.createSpyObj({
+ findById: observableOf(new RemoteData(false, false, true, undefined, mockItem))
+ });
+
+ routeStub = {
+ data: observableOf({
+ item: new RemoteData(false, false, true, null, {
+ id: 'fake-id'
+ })
+ })
+ };
+
+ notificationsServiceStub = new NotificationsServiceStub();
+
+ TestBed.configureTestingModule({
+ imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [MySimpleItemActionComponent],
+ providers: [
+ {provide: ActivatedRoute, useValue: routeStub},
+ {provide: Router, useValue: routerStub},
+ {provide: ItemDataService, useValue: mockItemDataService},
+ {provide: NotificationsService, useValue: notificationsServiceStub},
+ ], schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ successfulRestResponse = new RestResponse(true, '200');
+ failRestResponse = new RestResponse(false, '500');
+
+ fixture = TestBed.createComponent(MySimpleItemActionComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should render a page with messages based on the provided messageKey', () => {
+ const header = fixture.debugElement.query(By.css('h2')).nativeElement;
+ expect(header.innerHTML).toContain('item.edit.myEditAction.header');
+
+ const description = fixture.debugElement.query(By.css('p')).nativeElement;
+ expect(description.innerHTML).toContain('item.edit.myEditAction.description');
+
+ const confirmButton = fixture.debugElement.query(By.css('button.perform-action')).nativeElement;
+ expect(confirmButton.innerHTML).toContain('item.edit.myEditAction.confirm');
+
+ const cancelButton = fixture.debugElement.query(By.css('button.cancel')).nativeElement;
+ expect(cancelButton.innerHTML).toContain('item.edit.myEditAction.cancel');
+ });
+
+ it('should perform action when the button is clicked', () => {
+ spyOn(comp, 'performAction');
+ const performButton = fixture.debugElement.query(By.css('.perform-action'));
+ performButton.triggerEventHandler('click', null);
+
+ expect(comp.performAction).toHaveBeenCalled();
+ });
+
+ it('should process a RestResponse to navigate and display success notification', () => {
+ spyOn(notificationsServiceStub, 'success');
+ comp.processRestResponse(successfulRestResponse);
+
+ expect(notificationsServiceStub.success).toHaveBeenCalled();
+ expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditPath(mockItem.id)]);
+ });
+
+ it('should process a RestResponse to navigate and display success notification', () => {
+ spyOn(notificationsServiceStub, 'error');
+ comp.processRestResponse(failRestResponse);
+
+ expect(notificationsServiceStub.error).toHaveBeenCalled();
+ expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditPath(mockItem.id)]);
+ });
+
+});
diff --git a/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts
new file mode 100644
index 0000000000..743b52921f
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/simple-item-action/abstract-simple-item-action.component.ts
@@ -0,0 +1,84 @@
+import {Component, OnInit, Predicate} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {ItemDataService} from '../../../core/data/item-data.service';
+import {TranslateService} from '@ngx-translate/core';
+import {Item} from '../../../core/shared/item.model';
+import {RemoteData} from '../../../core/data/remote-data';
+import {Observable} from 'rxjs';
+import {getSucceededRemoteData} from '../../../core/shared/operators';
+import {first, map} from 'rxjs/operators';
+import {RestResponse} from '../../../core/cache/response-cache.models';
+import {findSuccessfulAccordingTo} from '../edit-item-operators';
+import {getItemEditPath} from '../../item-page-routing.module';
+
+/**
+ * Component to render and handle simple item edit actions such as withdrawal and reinstatement.
+ * This component is not meant to be used itself but to be extended.
+ */
+@Component({
+ selector: 'ds-simple-action',
+ templateUrl: './abstract-simple-item-action.component.html'
+})
+export class AbstractSimpleItemActionComponent implements OnInit {
+
+ itemRD$: Observable>;
+ item: Item;
+
+ protected messageKey: string;
+ confirmMessage: string;
+ cancelMessage: string;
+ headerMessage: string;
+ descriptionMessage: string;
+
+ protected predicate: Predicate>;
+
+ constructor(protected route: ActivatedRoute,
+ protected router: Router,
+ protected notificationsService: NotificationsService,
+ protected itemDataService: ItemDataService,
+ protected translateService: TranslateService) {
+ }
+
+ ngOnInit(): void {
+ this.itemRD$ = this.route.data.pipe(
+ map((data) => data.item),
+ getSucceededRemoteData()
+ )as Observable>;
+
+ this.itemRD$.pipe(first()).subscribe((rd) => {
+ this.item = rd.payload;
+ }
+ );
+
+ this.confirmMessage = 'item.edit.' + this.messageKey + '.confirm';
+ this.cancelMessage = 'item.edit.' + this.messageKey + '.cancel';
+ this.headerMessage = 'item.edit.' + this.messageKey + '.header';
+ this.descriptionMessage = 'item.edit.' + this.messageKey + '.description';
+ }
+
+ /**
+ * Perform the operation linked to this action
+ */
+ performAction() {
+ // Overwrite in subclasses
+ };
+
+ /**
+ * Process the response obtained during the performAction method and navigate back to the edit page
+ * @param response from the action in the performAction method
+ */
+ processRestResponse(response: RestResponse) {
+ if (response.isSuccessful) {
+ this.itemDataService.findById(this.item.id).pipe(
+ findSuccessfulAccordingTo(this.predicate)).subscribe(() => {
+ this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success'));
+ this.router.navigate([getItemEditPath(this.item.id)]);
+ });
+ } else {
+ this.notificationsService.error(this.translateService.get('item.edit.' + this.messageKey + '.error'));
+ this.router.navigate([getItemEditPath(this.item.id)]);
+ }
+ }
+
+}
diff --git a/src/app/+item-page/item-page-routing.module.ts b/src/app/+item-page/item-page-routing.module.ts
index 96158b867e..8c1f317bb7 100644
--- a/src/app/+item-page/item-page-routing.module.ts
+++ b/src/app/+item-page/item-page-routing.module.ts
@@ -4,6 +4,18 @@ 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 {URLCombiner} from '../core/url-combiner/url-combiner';
+import {getItemModulePath} from '../app-routing.module';
+
+export function getItemPageRoute(itemId: string) {
+ return new URLCombiner(getItemModulePath(), itemId).toString();
+}
+export function getItemEditPath(id: string) {
+ return new URLCombiner(getItemModulePath(),ITEM_EDIT_PATH.replace(/:id/, id)).toString()
+}
+
+const ITEM_EDIT_PATH = ':id/edit';
@NgModule({
imports: [
@@ -22,6 +34,11 @@ import { ItemPageResolver } from './item-page.resolver';
resolve: {
item: ItemPageResolver
}
+ },
+ {
+ path: ITEM_EDIT_PATH,
+ loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
+ canActivate: [AuthenticatedGuard]
}
])
],
diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts
index b4e5ba1cd8..dfd997dc8a 100644
--- a/src/app/+item-page/item-page.module.ts
+++ b/src/app/+item-page/item-page.module.ts
@@ -29,11 +29,13 @@ import { JournalComponent } from './simple/item-types/journal/journal.component'
import { JournalVolumeComponent } from './simple/item-types/journal-volume/journal-volume.component';
import { JournalIssueComponent } from './simple/item-types/journal-issue/journal-issue.component';
import { ItemComponent } from './simple/item-types/shared/item.component';
+import { EditItemPageModule } from './edit-item-page/edit-item-page.module';
@NgModule({
imports: [
CommonModule,
SharedModule,
+ EditItemPageModule,
ItemPageRoutingModule,
SearchPageModule
],
diff --git a/src/app/+logout-page/logout-page.component.html b/src/app/+logout-page/logout-page.component.html
index 9c6185b665..b5012ed53b 100644
--- a/src/app/+logout-page/logout-page.component.html
+++ b/src/app/+logout-page/logout-page.component.html
@@ -1,6 +1,6 @@
-
+
{{"logout.form.header" | translate}}
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-filter.component.html
index f5dc5fff38..1013bf7e28 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.component.html
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.html
@@ -1,5 +1,5 @@
-
{{'search.filters.filter.' + filter.name + '.head'| translate}}
{{'search.filters.filter.' + filter.name + '.head'| translate}}
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.scss b/src/app/+search-page/search-filters/search-filter/search-filter.component.scss
index 6e49172a48..4e45f49468 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.component.scss
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.scss
@@ -2,11 +2,12 @@
@import '../../../../styles/mixins.scss';
:host {
- border: 1px solid map-get($theme-colors, light);
- .search-filter-wrapper.closed {
- overflow: hidden;
- }
- .filter-toggle {
- line-height: $line-height-base;
- }
-}
\ No newline at end of file
+ border: 1px solid map-get($theme-colors, light);
+ cursor: pointer;
+ .search-filter-wrapper.closed {
+ overflow: hidden;
+ }
+ .filter-toggle {
+ line-height: $line-height-base;
+ }
+}
diff --git a/src/app/+search-page/search-filters/search-filters.component.ts b/src/app/+search-page/search-filters/search-filters.component.ts
index 7f1eb513ea..f16faff1f3 100644
--- a/src/app/+search-page/search-filters/search-filters.component.ts
+++ b/src/app/+search-page/search-filters/search-filters.component.ts
@@ -58,7 +58,6 @@ export class SearchFiltersComponent {
* @returns {Observable} Emits true whenever a given filter config should be shown
*/
isActive(filterConfig: SearchFilterConfig): Observable {
- // console.log(filter.name);
return this.filterService.getSelectedValuesForFilter(filterConfig).pipe(
mergeMap((isActive) => {
if (isNotEmpty(isActive)) {
diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html
index cbb82a59c6..adab27d8e9 100644
--- a/src/app/+search-page/search-page.component.html
+++ b/src/app/+search-page/search-page.component.html
@@ -26,7 +26,7 @@
diff --git a/src/app/+search-page/search-sidebar/search-sidebar.component.html b/src/app/+search-page/search-sidebar/search-sidebar.component.html
index 71959b558b..5ff1e3c8fa 100644
--- a/src/app/+search-page/search-sidebar/search-sidebar.component.html
+++ b/src/app/+search-page/search-sidebar/search-sidebar.component.html
@@ -4,7 +4,7 @@