diff --git a/README.md b/README.md
index 285f8829fd..8f2320dbf3 100644
--- a/README.md
+++ b/README.md
@@ -23,9 +23,6 @@ git clone https://github.com/DSpace/dspace-angular.git
# change directory to our repo
cd dspace-angular
-# install the global dependencies
-yarn run global
-
# install the local dependencies
yarn install
diff --git a/package.json b/package.json
index 69744b9729..7ded007e83 100644
--- a/package.json
+++ b/package.json
@@ -104,8 +104,10 @@
"font-awesome": "4.7.0",
"http-server": "0.11.1",
"https": "1.0.0",
+ "js-cookie": "2.2.0",
"js.clone": "0.0.3",
"jsonschema": "1.2.2",
+ "jwt-decode": "^2.2.0",
"methods": "1.1.2",
"morgan": "1.9.0",
"ng2-file-upload": "1.2.1",
@@ -133,6 +135,7 @@
"@types/express-serve-static-core": "4.11.1",
"@types/hammerjs": "2.0.35",
"@types/jasmine": "^2.8.6",
+ "@types/js-cookie": "2.1.0",
"@types/memory-cache": "0.2.0",
"@types/mime": "2.0.0",
"@types/node": "^9.4.6",
diff --git a/resources/i18n/en.json b/resources/i18n/en.json
index a8a45c5dd2..ba70b87e12 100644
--- a/resources/i18n/en.json
+++ b/resources/i18n/en.json
@@ -46,7 +46,9 @@
}
},
"nav": {
- "home": "Home"
+ "home": "Home",
+ "login": "Log In",
+ "logout": "Log Out"
},
"pagination": {
"results-per-page": "Results Per Page",
@@ -133,6 +135,55 @@
}
}
},
+ "admin": {
+ "registries": {
+ "metadata": {
+ "title": "DSpace Angular :: Metadata Registry",
+ "head": "Metadata Registry",
+ "description": "The metadata registry maintains a list of all metadata fields available in the repository. These fields may be divided amongst multiple schemas. However, DSpace requires the qualified Dublin Core schema.",
+ "schemas": {
+ "table": {
+ "id": "ID",
+ "namespace": "Namespace",
+ "name": "Name"
+ },
+ "no-items": "No metadata schemas to show."
+ }
+ },
+ "schema": {
+ "title": "DSpace Angular :: Metadata Schema Registry",
+ "head": "Metadata Schema",
+ "description": "This is the metadata schema for \"{{namespace}}\".",
+ "fields": {
+ "head": "Schema metadata fields",
+ "table": {
+ "field": "Field",
+ "scopenote": "Scope Note"
+ },
+ "no-items": "No metadata fields to show."
+ }
+ },
+ "bitstream-formats": {
+ "title": "DSpace Angular :: Bitstream Format Registry",
+ "head": "Bitstream Format Registry",
+ "description": "This list of bitstream formats provides information about known formats and their support level.",
+ "formats": {
+ "table": {
+ "name": "Name",
+ "mimetype": "MIME Type",
+ "supportLevel": {
+ "head": "Support Level",
+ "0": "Unknown",
+ "1": "Known",
+ "2": "Support"
+ },
+ "internal": "internal"
+ },
+ "no-items": "No bitstream formats to show."
+ }
+ }
+ }
+ },
"loading": {
"default": "Loading...",
"top-level-communities": "Loading top level communities...",
@@ -175,5 +226,31 @@
"group-expand": "Expand",
"group-collapse-help": "Click here to collapse",
"group-expand-help": "Click here to expand and add more element"
+ },
+ "login": {
+ "title": "Login",
+ "form": {
+ "header": "Please log in to DSpace",
+ "email": "Email address",
+ "forgot-password": "Have you forgotten your password?",
+ "new-user": "New user? Click here to register.",
+ "password": "Password",
+ "submit": "Log in"
+ }
+ },
+ "logout": {
+ "title": "Logout",
+ "form": {
+ "header": "Log out from DSpace",
+ "submit": "Log out"
+ }
+ },
+ "auth": {
+ "messages": {
+ "expired": "Your session has expired. Please log in again."
+ },
+ "errors": {
+ "invalid-user": "Invalid email or password."
+ }
}
}
diff --git a/src/app/+admin/admin-registries/admin-registries-routing.module.ts b/src/app/+admin/admin-registries/admin-registries-routing.module.ts
new file mode 100644
index 0000000000..8e3c322bc8
--- /dev/null
+++ b/src/app/+admin/admin-registries/admin-registries-routing.module.ts
@@ -0,0 +1,18 @@
+import { MetadataRegistryComponent } from './metadata-registry/metadata-registry.component';
+import { RouterModule } from '@angular/router';
+import { NgModule } from '@angular/core';
+import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component';
+import { BitstreamFormatsComponent } from './bitstream-formats/bitstream-formats.component';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild([
+ { path: 'metadata', component: MetadataRegistryComponent, data: { title: 'admin.registries.metadata.title' } },
+ { path: 'metadata/:schemaName', component: MetadataSchemaComponent, data: { title: 'admin.registries.schema.title' } },
+ { path: 'bitstream-formats', component: BitstreamFormatsComponent, data: { title: 'admin.registries.bitstream-formats.title' } },
+ ])
+ ]
+})
+export class AdminRegistriesRoutingModule {
+
+}
diff --git a/src/app/+admin/admin-registries/admin-registries.module.ts b/src/app/+admin/admin-registries/admin-registries.module.ts
new file mode 100644
index 0000000000..8ff42646ac
--- /dev/null
+++ b/src/app/+admin/admin-registries/admin-registries.module.ts
@@ -0,0 +1,27 @@
+import { NgModule } from '@angular/core';
+import { MetadataRegistryComponent } from './metadata-registry/metadata-registry.component';
+import { AdminRegistriesRoutingModule } from './admin-registries-routing.module';
+import { CommonModule } from '@angular/common';
+import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component';
+import { RouterModule } from '@angular/router';
+import { TranslateModule } from '@ngx-translate/core';
+import { BitstreamFormatsComponent } from './bitstream-formats/bitstream-formats.component';
+import { SharedModule } from '../../shared/shared.module';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ SharedModule,
+ RouterModule,
+ TranslateModule,
+ AdminRegistriesRoutingModule
+ ],
+ declarations: [
+ MetadataRegistryComponent,
+ MetadataSchemaComponent,
+ BitstreamFormatsComponent
+ ]
+})
+export class AdminRegistriesModule {
+
+}
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.html b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.html
new file mode 100644
index 0000000000..1ac547653f
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.html
@@ -0,0 +1,42 @@
+
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts
new file mode 100644
index 0000000000..f720c336e5
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts
@@ -0,0 +1,98 @@
+import { BitstreamFormatsComponent } from './bitstream-formats.component';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RegistryService } from '../../../core/registry/registry.service';
+import { Observable } from 'rxjs/Observable';
+import { RemoteData } from '../../../core/data/remote-data';
+import { PaginatedList } from '../../../core/data/paginated-list';
+import { CommonModule } from '@angular/common';
+import { RouterTestingModule } from '@angular/router/testing';
+import { TranslateModule } from '@ngx-translate/core';
+import { By } from '@angular/platform-browser';
+import { SharedModule } from '../../../shared/shared.module';
+import { PaginationComponent } from '../../../shared/pagination/pagination.component';
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
+import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
+import { HostWindowService } from '../../../shared/host-window.service';
+import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
+
+describe('BitstreamFormatsComponent', () => {
+ let comp: BitstreamFormatsComponent;
+ let fixture: ComponentFixture;
+ let registryService: RegistryService;
+ const mockFormatsList = [
+ {
+ shortDescription: 'Unknown',
+ description: 'Unknown data format',
+ mimetype: 'application/octet-stream',
+ supportLevel: 0,
+ internal: false,
+ extensions: null
+ },
+ {
+ shortDescription: 'License',
+ description: 'Item-specific license agreed upon to submission',
+ mimetype: 'text/plain; charset=utf-8',
+ supportLevel: 1,
+ internal: true,
+ extensions: null
+ },
+ {
+ shortDescription: 'CC License',
+ description: 'Item-specific Creative Commons license agreed upon to submission',
+ mimetype: 'text/html; charset=utf-8',
+ supportLevel: 2,
+ internal: true,
+ extensions: null
+ },
+ {
+ shortDescription: 'Adobe PDF',
+ description: 'Adobe Portable Document Format',
+ mimetype: 'application/pdf',
+ supportLevel: 0,
+ internal: false,
+ extensions: null
+ }
+ ];
+ const mockFormats = Observable.of(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockFormatsList)));
+ const registryServiceStub = {
+ getBitstreamFormats: () => mockFormats
+ };
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe],
+ providers: [
+ { provide: RegistryService, useValue: registryServiceStub },
+ { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BitstreamFormatsComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ registryService = (comp as any).service;
+ });
+
+ it('should contain four formats', () => {
+ const tbody: HTMLElement = fixture.debugElement.query(By.css('#formats>tbody')).nativeElement;
+ expect(tbody.children.length).toBe(4);
+ });
+
+ it('should contain the correct formats', () => {
+ const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(1)')).nativeElement;
+ expect(unknownName.textContent).toBe('Unknown');
+
+ const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(1)')).nativeElement;
+ expect(licenseName.textContent).toBe('License');
+
+ const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(1)')).nativeElement;
+ expect(ccLicenseName.textContent).toBe('CC License');
+
+ const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(1)')).nativeElement;
+ expect(adobeName.textContent).toBe('Adobe PDF');
+ });
+
+});
diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts
new file mode 100644
index 0000000000..d6c84ac858
--- /dev/null
+++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts
@@ -0,0 +1,33 @@
+import { Component } from '@angular/core';
+import { RegistryService } from '../../../core/registry/registry.service';
+import { Observable } from 'rxjs/Observable';
+import { RemoteData } from '../../../core/data/remote-data';
+import { PaginatedList } from '../../../core/data/paginated-list';
+import { BitstreamFormat } from '../../../core/registry/mock-bitstream-format.model';
+import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
+
+@Component({
+ selector: 'ds-bitstream-formats',
+ templateUrl: './bitstream-formats.component.html'
+})
+export class BitstreamFormatsComponent {
+
+ bitstreamFormats: Observable>>;
+ config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
+ id: 'registry-bitstreamformats-pagination',
+ pageSize: 10000
+ });
+
+ constructor(private registryService: RegistryService) {
+ this.updateFormats();
+ }
+
+ onPageChange(event) {
+ this.config.currentPage = event;
+ this.updateFormats();
+ }
+
+ private updateFormats() {
+ this.bitstreamFormats = this.registryService.getBitstreamFormats(this.config);
+ }
+}
diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.html b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.html
new file mode 100644
index 0000000000..49a52cec9c
--- /dev/null
+++ b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.html
@@ -0,0 +1,42 @@
+
diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts
new file mode 100644
index 0000000000..e3b2e1f2c1
--- /dev/null
+++ b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts
@@ -0,0 +1,72 @@
+import { MetadataRegistryComponent } from './metadata-registry.component';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { Observable } from 'rxjs/Observable';
+import { RemoteData } from '../../../core/data/remote-data';
+import { PaginatedList } from '../../../core/data/paginated-list';
+import { TranslateModule } from '@ngx-translate/core';
+import { By } from '@angular/platform-browser';
+import { CommonModule } from '@angular/common';
+import { RouterTestingModule } from '@angular/router/testing';
+import { RegistryService } from '../../../core/registry/registry.service';
+import { SharedModule } from '../../../shared/shared.module';
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
+import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
+import { PaginationComponent } from '../../../shared/pagination/pagination.component';
+import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
+import { HostWindowService } from '../../../shared/host-window.service';
+
+describe('MetadataRegistryComponent', () => {
+ let comp: MetadataRegistryComponent;
+ let fixture: ComponentFixture;
+ let registryService: RegistryService;
+ const mockSchemasList = [
+ {
+ id: 1,
+ self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1',
+ prefix: 'dc',
+ namespace: 'http://dublincore.org/documents/dcmi-terms/'
+ },
+ {
+ id: 2,
+ self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2',
+ prefix: 'mock',
+ namespace: 'http://dspace.org/mockschema'
+ }
+ ];
+ const mockSchemas = Observable.of(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockSchemasList)));
+ const registryServiceStub = {
+ getMetadataSchemas: () => mockSchemas
+ };
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [MetadataRegistryComponent, PaginationComponent, EnumKeysPipe],
+ providers: [
+ { provide: RegistryService, useValue: registryServiceStub },
+ { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MetadataRegistryComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ registryService = (comp as any).service;
+ });
+
+ it('should contain two schemas', () => {
+ const tbody: HTMLElement = fixture.debugElement.query(By.css('#metadata-schemas>tbody')).nativeElement;
+ expect(tbody.children.length).toBe(2);
+ });
+
+ it('should contain the correct schemas', () => {
+ const dcName: HTMLElement = fixture.debugElement.query(By.css('#metadata-schemas tr:nth-child(1) td:nth-child(3)')).nativeElement;
+ expect(dcName.textContent).toBe('dc');
+
+ const mockName: HTMLElement = fixture.debugElement.query(By.css('#metadata-schemas tr:nth-child(2) td:nth-child(3)')).nativeElement;
+ expect(mockName.textContent).toBe('mock');
+ });
+
+});
diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts
new file mode 100644
index 0000000000..15dc6b0d80
--- /dev/null
+++ b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts
@@ -0,0 +1,34 @@
+import { Component } from '@angular/core';
+import { RegistryService } from '../../../core/registry/registry.service';
+import { Observable } from 'rxjs/Observable';
+import { RemoteData } from '../../../core/data/remote-data';
+import { PaginatedList } from '../../../core/data/paginated-list';
+import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
+import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
+
+@Component({
+ selector: 'ds-metadata-registry',
+ templateUrl: './metadata-registry.component.html'
+})
+export class MetadataRegistryComponent {
+
+ metadataSchemas: Observable>>;
+ config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
+ id: 'registry-metadataschemas-pagination',
+ pageSize: 10000
+ });
+
+ constructor(private registryService: RegistryService) {
+ this.updateSchemas();
+ }
+
+ onPageChange(event) {
+ this.config.currentPage = event;
+ this.updateSchemas();
+ }
+
+ private updateSchemas() {
+ this.metadataSchemas = this.registryService.getMetadataSchemas(this.config);
+ }
+
+}
diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.html b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.html
new file mode 100644
index 0000000000..e9734888ae
--- /dev/null
+++ b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.html
@@ -0,0 +1,41 @@
+
diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts
new file mode 100644
index 0000000000..7e6064ddff
--- /dev/null
+++ b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts
@@ -0,0 +1,121 @@
+import { MetadataSchemaComponent } from './metadata-schema.component';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { Observable } from 'rxjs/Observable';
+import { RemoteData } from '../../../core/data/remote-data';
+import { PaginatedList } from '../../../core/data/paginated-list';
+import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
+import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
+import { CommonModule } from '@angular/common';
+import { ActivatedRoute, Router } from '@angular/router';
+import { By } from '@angular/platform-browser';
+import { MockTranslateLoader } from '../../../shared/testing/mock-translate-loader';
+import { RegistryService } from '../../../core/registry/registry.service';
+import { SharedModule } from '../../../shared/shared.module';
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
+import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
+import { PaginationComponent } from '../../../shared/pagination/pagination.component';
+import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
+import { HostWindowService } from '../../../shared/host-window.service';
+import { RouterStub } from '../../../shared/testing/router-stub';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ActivatedRouteStub } from '../../../shared/testing/active-router-stub';
+
+describe('MetadataSchemaComponent', () => {
+ let comp: MetadataSchemaComponent;
+ let fixture: ComponentFixture;
+ let registryService: RegistryService;
+ const mockSchemasList = [
+ {
+ id: 1,
+ self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/1',
+ prefix: 'dc',
+ namespace: 'http://dublincore.org/documents/dcmi-terms/'
+ },
+ {
+ id: 2,
+ self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadataschemas/2',
+ prefix: 'mock',
+ namespace: 'http://dspace.org/mockschema'
+ }
+ ];
+ const mockFieldsList = [
+ {
+ self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/8',
+ element: 'contributor',
+ qualifier: 'advisor',
+ scopenote: null,
+ schema: mockSchemasList[0]
+ },
+ {
+ self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/9',
+ element: 'contributor',
+ qualifier: 'author',
+ scopenote: null,
+ schema: mockSchemasList[0]
+ },
+ {
+ self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/10',
+ element: 'contributor',
+ qualifier: 'editor',
+ scopenote: 'test scope note',
+ schema: mockSchemasList[1]
+ },
+ {
+ self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/metadatafields/11',
+ element: 'contributor',
+ qualifier: 'illustrator',
+ scopenote: null,
+ schema: mockSchemasList[1]
+ }
+ ];
+ const mockSchemas = Observable.of(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockSchemasList)));
+ const registryServiceStub = {
+ getMetadataSchemas: () => mockSchemas,
+ getMetadataFieldsBySchema: (schema: MetadataSchema) => Observable.of(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockFieldsList.filter((value) => value.schema === schema)))),
+ getMetadataSchemaByName: (schemaName: string) => Observable.of(new RemoteData(false, false, true, undefined, mockSchemasList.filter((value) => value.prefix === schemaName)[0]))
+ };
+ const schemaNameParam = 'mock';
+ const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
+ params: Observable.of({
+ schemaName: schemaNameParam
+ })
+ });
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+ declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe],
+ providers: [
+ { provide: RegistryService, useValue: registryServiceStub },
+ { provide: ActivatedRoute, useValue: activatedRouteStub },
+ { provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
+ { provide: Router, useValue: new RouterStub() }
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MetadataSchemaComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ registryService = (comp as any).service;
+ });
+
+ it('should contain the schema prefix in the header', () => {
+ const header: HTMLElement = fixture.debugElement.query(By.css('.metadata-schema #header')).nativeElement;
+ expect(header.textContent).toContain('mock');
+ });
+
+ it('should contain two fields', () => {
+ const tbody: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields>tbody')).nativeElement;
+ expect(tbody.children.length).toBe(2);
+ });
+
+ it('should contain the correct fields', () => {
+ const editorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(1) td:nth-child(1)')).nativeElement;
+ expect(editorField.textContent).toBe('mock.contributor.editor');
+
+ const illustratorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(2) td:nth-child(1)')).nativeElement;
+ expect(illustratorField.textContent).toBe('mock.contributor.illustrator');
+ });
+});
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
new file mode 100644
index 0000000000..2f0bfdeddb
--- /dev/null
+++ b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts
@@ -0,0 +1,55 @@
+import { Component, OnInit } from '@angular/core';
+import { RegistryService } from '../../../core/registry/registry.service';
+import { ActivatedRoute } from '@angular/router';
+import { Observable } from 'rxjs/Observable';
+import { RemoteData } from '../../../core/data/remote-data';
+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',
+ templateUrl: './metadata-schema.component.html'
+})
+export class MetadataSchemaComponent implements OnInit {
+
+ namespace;
+
+ metadataSchema: Observable>;
+ metadataFields: Observable>>;
+ config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
+ id: 'registry-metadatafields-pagination',
+ pageSize: 10000
+ });
+
+ constructor(private registryService: RegistryService, private route: ActivatedRoute) {
+
+ }
+
+ ngOnInit(): void {
+ this.route.params.subscribe((params) => {
+ this.initialize(params);
+ });
+ }
+
+ initialize(params) {
+ this.metadataSchema = this.registryService.getMetadataSchemaByName(params.schemaName);
+ this.updateFields();
+ }
+
+ onPageChange(event) {
+ this.config.currentPage = event;
+ this.updateFields();
+ }
+
+ private updateFields() {
+ this.metadataSchema.subscribe((schemaData) => {
+ const schema = schemaData.payload;
+ this.metadataFields = this.registryService.getMetadataFieldsBySchema(schema, this.config);
+ this.namespace = { namespace: schemaData.payload.namespace };
+ });
+ }
+
+}
diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/+admin/admin-routing.module.ts
new file mode 100644
index 0000000000..e7c96bb9c4
--- /dev/null
+++ b/src/app/+admin/admin-routing.module.ts
@@ -0,0 +1,13 @@
+import { RouterModule } from '@angular/router';
+import { NgModule } from '@angular/core';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild([
+ { path: 'registries', loadChildren: './admin-registries/admin-registries.module#AdminRegistriesModule' }
+ ])
+ ]
+})
+export class AdminRoutingModule {
+
+}
diff --git a/src/app/+admin/admin.module.ts b/src/app/+admin/admin.module.ts
new file mode 100644
index 0000000000..b979813376
--- /dev/null
+++ b/src/app/+admin/admin.module.ts
@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { AdminRegistriesModule } from './admin-registries/admin-registries.module';
+import { AdminRoutingModule } from './admin-routing.module';
+
+@NgModule({
+ imports: [
+ AdminRegistriesModule,
+ AdminRoutingModule
+ ]
+})
+export class AdminModule {
+
+}
diff --git a/src/app/+login-page/login-page-routing.module.ts b/src/app/+login-page/login-page-routing.module.ts
new file mode 100644
index 0000000000..4e932c50ce
--- /dev/null
+++ b/src/app/+login-page/login-page-routing.module.ts
@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { LoginPageComponent } from './login-page.component';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild([
+ { path: '', component: LoginPageComponent, data: { title: 'login.title' } }
+ ])
+ ]
+})
+export class LoginPageRoutingModule { }
diff --git a/src/app/+login-page/login-page.component.html b/src/app/+login-page/login-page.component.html
new file mode 100644
index 0000000000..6dcb11fbb0
--- /dev/null
+++ b/src/app/+login-page/login-page.component.html
@@ -0,0 +1,9 @@
+
+
+
+

+
{{"login.form.header" | translate}}
+
+
+
+
diff --git a/src/app/+login-page/login-page.component.scss b/src/app/+login-page/login-page.component.scss
new file mode 100644
index 0000000000..38adf24671
--- /dev/null
+++ b/src/app/+login-page/login-page.component.scss
@@ -0,0 +1,6 @@
+@import '../../styles/variables.scss';
+
+.login-logo {
+ height: $login-logo-height;
+ width: $login-logo-width;
+}
diff --git a/src/app/+login-page/login-page.component.spec.ts b/src/app/+login-page/login-page.component.spec.ts
new file mode 100644
index 0000000000..609cf47794
--- /dev/null
+++ b/src/app/+login-page/login-page.component.spec.ts
@@ -0,0 +1,47 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { Store } from '@ngrx/store';
+import { TranslateModule } from '@ngx-translate/core';
+import { Observable } from 'rxjs/Observable';
+import 'rxjs/add/observable/of';
+
+import { LoginPageComponent } from './login-page.component';
+
+describe('LoginPageComponent', () => {
+ let comp: LoginPageComponent;
+ let fixture: ComponentFixture;
+
+ const store: Store = jasmine.createSpyObj('store', {
+ /* tslint:disable:no-empty */
+ dispatch: {},
+ /* tslint:enable:no-empty */
+ select: Observable.of(true)
+ });
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot()
+ ],
+ declarations: [LoginPageComponent],
+ providers: [
+ {
+ provide: Store, useValue: store
+ }
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LoginPageComponent);
+ comp = fixture.componentInstance; // SearchPageComponent test instance
+ fixture.detectChanges();
+ });
+
+ it('should create instance', () => {
+ expect(comp).toBeDefined()
+ });
+
+});
diff --git a/src/app/+login-page/login-page.component.ts b/src/app/+login-page/login-page.component.ts
new file mode 100644
index 0000000000..2752973130
--- /dev/null
+++ b/src/app/+login-page/login-page.component.ts
@@ -0,0 +1,21 @@
+import { Component, OnDestroy } from '@angular/core';
+
+import { Store } from '@ngrx/store';
+
+import { AppState } from '../app.reducer';
+import { ResetAuthenticationMessagesAction } from '../core/auth/auth.actions';
+
+@Component({
+ selector: 'ds-login-page',
+ styleUrls: ['./login-page.component.scss'],
+ templateUrl: './login-page.component.html'
+})
+export class LoginPageComponent implements OnDestroy {
+
+ constructor(private store: Store) {}
+
+ ngOnDestroy() {
+ // Clear all authentication messages when leaving login page
+ this.store.dispatch(new ResetAuthenticationMessagesAction());
+ }
+}
diff --git a/src/app/+login-page/login-page.module.ts b/src/app/+login-page/login-page.module.ts
new file mode 100644
index 0000000000..4d3f726c40
--- /dev/null
+++ b/src/app/+login-page/login-page.module.ts
@@ -0,0 +1,19 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { SharedModule } from '../shared/shared.module';
+import { LoginPageComponent } from './login-page.component';
+import { LoginPageRoutingModule } from './login-page-routing.module';
+
+@NgModule({
+ imports: [
+ LoginPageRoutingModule,
+ CommonModule,
+ SharedModule,
+ ],
+ declarations: [
+ LoginPageComponent
+ ]
+})
+export class LoginPageModule {
+
+}
diff --git a/src/app/+logout-page/logout-page-routing.module.ts b/src/app/+logout-page/logout-page-routing.module.ts
new file mode 100644
index 0000000000..64894c1f87
--- /dev/null
+++ b/src/app/+logout-page/logout-page-routing.module.ts
@@ -0,0 +1,19 @@
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { LogoutPageComponent } from './logout-page.component';
+import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild([
+ {
+ canActivate: [AuthenticatedGuard],
+ path: '',
+ component: LogoutPageComponent,
+ data: { title: 'logout.title' }
+ }
+ ])
+ ]
+})
+export class LogoutPageRoutingModule { }
diff --git a/src/app/+logout-page/logout-page.component.html b/src/app/+logout-page/logout-page.component.html
new file mode 100644
index 0000000000..9c6185b665
--- /dev/null
+++ b/src/app/+logout-page/logout-page.component.html
@@ -0,0 +1,9 @@
+
+
+
+

+
{{"logout.form.header" | translate}}
+
+
+
+
diff --git a/src/app/+logout-page/logout-page.component.scss b/src/app/+logout-page/logout-page.component.scss
new file mode 100644
index 0000000000..7e594c0d9b
--- /dev/null
+++ b/src/app/+logout-page/logout-page.component.scss
@@ -0,0 +1 @@
+@import '../+login-page/login-page.component.scss';
diff --git a/src/app/+logout-page/logout-page.component.spec.ts b/src/app/+logout-page/logout-page.component.spec.ts
new file mode 100644
index 0000000000..5fd4e076f2
--- /dev/null
+++ b/src/app/+logout-page/logout-page.component.spec.ts
@@ -0,0 +1,31 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { TranslateModule } from '@ngx-translate/core';
+
+import { LogoutPageComponent } from './logout-page.component';
+
+describe('LogoutPageComponent', () => {
+ let comp: LogoutPageComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot()
+ ],
+ declarations: [LogoutPageComponent],
+ schemas: [NO_ERRORS_SCHEMA]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LogoutPageComponent);
+ comp = fixture.componentInstance; // SearchPageComponent test instance
+ fixture.detectChanges();
+ });
+
+ it('should create instance', () => {
+ expect(comp).toBeDefined()
+ });
+
+});
diff --git a/src/app/+logout-page/logout-page.component.ts b/src/app/+logout-page/logout-page.component.ts
new file mode 100644
index 0000000000..4fa4b9900a
--- /dev/null
+++ b/src/app/+logout-page/logout-page.component.ts
@@ -0,0 +1,10 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'ds-logout-page',
+ styleUrls: ['./logout-page.component.scss'],
+ templateUrl: './logout-page.component.html'
+})
+export class LogoutPageComponent {
+
+}
diff --git a/src/app/+logout-page/logout-page.module.ts b/src/app/+logout-page/logout-page.module.ts
new file mode 100644
index 0000000000..b085a5117b
--- /dev/null
+++ b/src/app/+logout-page/logout-page.module.ts
@@ -0,0 +1,19 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { SharedModule } from '../shared/shared.module';
+import { LogoutPageComponent } from './logout-page.component';
+import { LogoutPageRoutingModule } from './logout-page-routing.module';
+
+@NgModule({
+ imports: [
+ LogoutPageRoutingModule,
+ CommonModule,
+ SharedModule,
+ ],
+ declarations: [
+ LogoutPageComponent
+ ]
+})
+export class LogoutPageModule {
+
+}
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts
index 44d9c7e709..695e0204f2 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts
@@ -14,7 +14,7 @@ import {
import { hasValue, isEmpty, isNotEmpty, } from '../../../shared/empty.util';
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
import { SearchService } from '../../search-service/search.service';
-import { RouteService } from '../../../shared/route.service';
+import { RouteService } from '../../../shared/services/route.service';
import ObjectExpression from 'rollup/dist/typings/ast/nodes/ObjectExpression';
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
diff --git a/src/app/+search-page/search-options.model.ts b/src/app/+search-page/search-options.model.ts
index df8d8e713a..4b4987a096 100644
--- a/src/app/+search-page/search-options.model.ts
+++ b/src/app/+search-page/search-options.model.ts
@@ -1,6 +1,6 @@
import { isNotEmpty } from '../shared/empty.util';
import { URLCombiner } from '../core/url-combiner/url-combiner';
-import 'core-js/fn/object/entries';
+import 'core-js/library/fn/object/entries';
export enum ViewMode {
List = 'list',
diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html
index 1a1f379920..e8dee94139 100644
--- a/src/app/+search-page/search-page.component.html
+++ b/src/app/+search-page/search-page.component.html
@@ -1,8 +1,8 @@
-
+
-
diff --git a/src/app/header/header.component.spec.ts b/src/app/header/header.component.spec.ts
index 7c1a9601ac..87fa2995d6 100644
--- a/src/app/header/header.component.spec.ts
+++ b/src/app/header/header.component.spec.ts
@@ -9,6 +9,16 @@ import { Observable } from 'rxjs/Observable';
import { HeaderComponent } from './header.component';
import { HeaderState } from './header.reducer';
import { HeaderToggleAction } from './header.actions';
+import { AuthNavMenuComponent } from '../shared/auth-nav-menu/auth-nav-menu.component';
+import { LogInComponent } from '../shared/log-in/log-in.component';
+import { LogOutComponent } from '../shared/log-out/log-out.component';
+import { LoadingComponent } from '../shared/loading/loading.component';
+import { ReactiveFormsModule } from '@angular/forms';
+import { HostWindowService } from '../shared/host-window.service';
+import { HostWindowServiceStub } from '../shared/testing/host-window-service-stub';
+import { RouterStub } from '../shared/testing/router-stub';
+import { Router } from '@angular/router';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
let comp: HeaderComponent;
let fixture: ComponentFixture
;
@@ -19,8 +29,17 @@ describe('HeaderComponent', () => {
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
- imports: [StoreModule.forRoot({}), TranslateModule.forRoot(), NgbCollapseModule.forRoot()],
- declarations: [HeaderComponent]
+ imports: [
+ StoreModule.forRoot({}),
+ TranslateModule.forRoot(),
+ NgbCollapseModule.forRoot(),
+ NoopAnimationsModule,
+ ReactiveFormsModule],
+ declarations: [HeaderComponent, AuthNavMenuComponent, LoadingComponent, LogInComponent, LogOutComponent],
+ providers: [
+ { provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
+ { provide: Router, useClass: RouterStub },
+ ]
})
.compileComponents(); // compile template and css
}));
diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts
index 624ae209dd..93cb329f4f 100644
--- a/src/app/header/header.component.ts
+++ b/src/app/header/header.component.ts
@@ -1,10 +1,12 @@
import { Component, OnInit } from '@angular/core';
import { createSelector, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
+import { RouterReducerState } from '@ngrx/router-store';
import { HeaderState } from './header.reducer';
import { HeaderToggleAction } from './header.actions';
import { AppState } from '../app.reducer';
+import { HostWindowService } from '../shared/host-window.service';
const headerStateSelector = (state: AppState) => state.header;
const navCollapsedSelector = createSelector(headerStateSelector, (header: HeaderState) => header.navCollapsed);
@@ -12,17 +14,25 @@ const navCollapsedSelector = createSelector(headerStateSelector, (header: Header
@Component({
selector: 'ds-header',
styleUrls: ['header.component.scss'],
- templateUrl: 'header.component.html'
+ templateUrl: 'header.component.html',
})
export class HeaderComponent implements OnInit {
+ /**
+ * Whether user is authenticated.
+ * @type {Observable}
+ */
+ public isAuthenticated: Observable;
public isNavBarCollapsed: Observable;
+ public showAuth = false;
constructor(
- private store: Store
+ private store: Store,
+ private windowService: HostWindowService
) {
}
ngOnInit(): void {
+ // set loading
this.isNavBarCollapsed = this.store.select(navCollapsedSelector);
}
diff --git a/src/app/pagenotfound/pagenotfound.component.ts b/src/app/pagenotfound/pagenotfound.component.ts
index bd119a4de9..e7923b3466 100644
--- a/src/app/pagenotfound/pagenotfound.component.ts
+++ b/src/app/pagenotfound/pagenotfound.component.ts
@@ -1,4 +1,4 @@
-import { ServerResponseService } from '../shared/server-response.service';
+import { ServerResponseService } from '../shared/services/server-response.service';
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html
new file mode 100644
index 0000000000..cc9b8c410b
--- /dev/null
+++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html
@@ -0,0 +1,26 @@
+
+
+
diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.scss b/src/app/shared/auth-nav-menu/auth-nav-menu.component.scss
new file mode 100644
index 0000000000..a8c7b84f56
--- /dev/null
+++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.scss
@@ -0,0 +1,8 @@
+#loginDropdownMenu, #logoutDropdownMenu {
+ min-width: 330px;
+ z-index: 1002;
+}
+
+#loginDropdownMenu {
+ min-height: 260px;
+}
diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts
new file mode 100644
index 0000000000..8b9f7c8775
--- /dev/null
+++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts
@@ -0,0 +1,297 @@
+import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
+import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
+
+import { By } from '@angular/platform-browser';
+import { Store, StoreModule } from '@ngrx/store';
+
+import { authReducer, AuthState } from '../../core/auth/auth.reducer';
+import { EpersonMock } from '../testing/eperson-mock';
+import { TranslateModule } from '@ngx-translate/core';
+import { AppState } from '../../app.reducer';
+import { AuthNavMenuComponent } from './auth-nav-menu.component';
+import { HostWindowServiceStub } from '../testing/host-window-service-stub';
+import { HostWindowService } from '../host-window.service';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
+
+describe('AuthNavMenuComponent', () => {
+
+ let component: AuthNavMenuComponent;
+ let deNavMenu: DebugElement;
+ let deNavMenuItem: DebugElement;
+ let fixture: ComponentFixture;
+
+ const notAuthState: AuthState = {
+ authenticated: false,
+ loaded: false,
+ loading: false
+ };
+ const authState: AuthState = {
+ authenticated: true,
+ loaded: true,
+ loading: false,
+ authToken: new AuthTokenInfo('test_token'),
+ user: EpersonMock
+ };
+ let routerState = {
+ url: '/home'
+ };
+
+ describe('when is a not mobile view', () => {
+ beforeEach(async(() => {
+ const window = new HostWindowServiceStub(800);
+
+ // refine the test module by declaring the test component
+ TestBed.configureTestingModule({
+ imports: [
+ NoopAnimationsModule,
+ StoreModule.forRoot(authReducer),
+ TranslateModule.forRoot()
+ ],
+ declarations: [
+ AuthNavMenuComponent
+ ],
+ providers: [
+ {provide: HostWindowService, useValue: window},
+ ],
+ schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ })
+ .compileComponents();
+
+ }));
+
+ describe('when route is /login and user is not authenticated', () => {
+ routerState = {
+ url: '/login'
+ };
+ beforeEach(inject([Store], (store: Store) => {
+ store
+ .subscribe((state) => {
+ (state as any).router = Object.create({});
+ (state as any).router.state = routerState;
+ (state as any).core = Object.create({});
+ (state as any).core.auth = notAuthState;
+ });
+
+ // create component and test fixture
+ fixture = TestBed.createComponent(AuthNavMenuComponent);
+
+ // get test component from the fixture
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+
+ const navMenuSelector = '.navbar-nav';
+ deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
+
+ const navMenuItemSelector = 'li';
+ deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
+ }));
+
+ it('should not render', () => {
+ expect(component).toBeTruthy();
+ expect(deNavMenu.nativeElement).toBeDefined();
+ expect(deNavMenuItem).toBeNull();
+ });
+
+ });
+
+ describe('when route is /logout and user is authenticated', () => {
+ routerState = {
+ url: '/logout'
+ };
+ beforeEach(inject([Store], (store: Store) => {
+ store
+ .subscribe((state) => {
+ (state as any).router = Object.create({});
+ (state as any).router.state = routerState;
+ (state as any).core = Object.create({});
+ (state as any).core.auth = authState;
+ });
+
+ // create component and test fixture
+ fixture = TestBed.createComponent(AuthNavMenuComponent);
+
+ // get test component from the fixture
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+
+ const navMenuSelector = '.navbar-nav';
+ deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
+
+ const navMenuItemSelector = 'li';
+ deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
+ }));
+
+ it('should not render', () => {
+ expect(component).toBeTruthy();
+ expect(deNavMenu.nativeElement).toBeDefined();
+ expect(deNavMenuItem).toBeNull();
+ });
+
+ });
+
+ describe('when route is not /login neither /logout', () => {
+ describe('when user is not authenticated', () => {
+
+ beforeEach(inject([Store], (store: Store) => {
+ routerState = {
+ url: '/home'
+ };
+ store
+ .subscribe((state) => {
+ (state as any).router = Object.create({});
+ (state as any).router.state = routerState;
+ (state as any).core = Object.create({});
+ (state as any).core.auth = notAuthState;
+ });
+
+ // create component and test fixture
+ fixture = TestBed.createComponent(AuthNavMenuComponent);
+
+ // get test component from the fixture
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+
+ const navMenuSelector = '.navbar-nav';
+ deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
+
+ const navMenuItemSelector = 'li';
+ deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
+ }));
+
+ it('should render login dropdown menu', () => {
+ const loginDropdownMenu = deNavMenuItem.query(By.css('div[id=loginDropdownMenu]'));
+ expect(loginDropdownMenu.nativeElement).toBeDefined();
+ });
+ });
+
+ describe('when user is authenticated', () => {
+ beforeEach(inject([Store], (store: Store) => {
+ routerState = {
+ url: '/home'
+ };
+ store
+ .subscribe((state) => {
+ (state as any).router = Object.create({});
+ (state as any).router.state = routerState;
+ (state as any).core = Object.create({});
+ (state as any).core.auth = authState;
+ });
+
+ // create component and test fixture
+ fixture = TestBed.createComponent(AuthNavMenuComponent);
+
+ // get test component from the fixture
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+
+ const navMenuSelector = '.navbar-nav';
+ deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
+
+ const navMenuItemSelector = 'li';
+ deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
+ }));
+
+ it('should render logout dropdown menu', () => {
+ const logoutDropdownMenu = deNavMenuItem.query(By.css('div[id=logoutDropdownMenu]'));
+ expect(logoutDropdownMenu.nativeElement).toBeDefined();
+ });
+ })
+ })
+ });
+
+ describe('when is a mobile view', () => {
+ beforeEach(async(() => {
+ const window = new HostWindowServiceStub(300);
+
+ // refine the test module by declaring the test component
+ TestBed.configureTestingModule({
+ imports: [
+ NoopAnimationsModule,
+ StoreModule.forRoot(authReducer),
+ TranslateModule.forRoot()
+ ],
+ declarations: [
+ AuthNavMenuComponent
+ ],
+ providers: [
+ {provide: HostWindowService, useValue: window},
+ ],
+ schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ })
+ .compileComponents();
+
+ }));
+
+ describe('when user is not authenticated', () => {
+
+ beforeEach(inject([Store], (store: Store) => {
+ store
+ .subscribe((state) => {
+ (state as any).router = Object.create({});
+ (state as any).router.state = routerState;
+ (state as any).core = Object.create({});
+ (state as any).core.auth = notAuthState;
+ });
+
+ // create component and test fixture
+ fixture = TestBed.createComponent(AuthNavMenuComponent);
+
+ // get test component from the fixture
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+
+ const navMenuSelector = '.navbar-nav';
+ deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
+
+ const navMenuItemSelector = 'li';
+ deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
+ }));
+
+ it('should render login link', () => {
+ const loginDropdownMenu = deNavMenuItem.query(By.css('a[id=loginLink]'));
+ expect(loginDropdownMenu.nativeElement).toBeDefined();
+ });
+ });
+
+ describe('when user is authenticated', () => {
+ beforeEach(inject([Store], (store: Store) => {
+ store
+ .subscribe((state) => {
+ (state as any).router = Object.create({});
+ (state as any).router.state = routerState;
+ (state as any).core = Object.create({});
+ (state as any).core.auth = authState;
+ });
+
+ // create component and test fixture
+ fixture = TestBed.createComponent(AuthNavMenuComponent);
+
+ // get test component from the fixture
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+
+ const navMenuSelector = '.navbar-nav';
+ deNavMenu = fixture.debugElement.query(By.css(navMenuSelector));
+
+ const navMenuItemSelector = 'li';
+ deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector));
+ }));
+
+ it('should render logout link', inject([Store], (store: Store) => {
+ const logoutDropdownMenu = deNavMenuItem.query(By.css('a[id=logoutLink]'));
+ expect(logoutDropdownMenu.nativeElement).toBeDefined();
+ }));
+ })
+ })
+});
diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts
new file mode 100644
index 0000000000..1c376258fb
--- /dev/null
+++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts
@@ -0,0 +1,59 @@
+import { Component, OnInit } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { RouterReducerState } from '@ngrx/router-store';
+import { Store } from '@ngrx/store';
+
+import { fadeInOut, fadeOut } from '../animations/fade';
+import { HostWindowService } from '../host-window.service';
+import { AppState, routerStateSelector } from '../../app.reducer';
+import { isNotUndefined } from '../empty.util';
+import { getAuthenticatedUser, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors';
+import { Eperson } from '../../core/eperson/models/eperson.model';
+import { LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service';
+
+@Component({
+ selector: 'ds-auth-nav-menu',
+ templateUrl: './auth-nav-menu.component.html',
+ styleUrls: ['./auth-nav-menu.component.scss'],
+ animations: [fadeInOut, fadeOut]
+})
+export class AuthNavMenuComponent implements OnInit {
+ /**
+ * Whether user is authenticated.
+ * @type {Observable}
+ */
+ public isAuthenticated: Observable;
+
+ /**
+ * True if the authentication is loading.
+ * @type {boolean}
+ */
+ public loading: Observable;
+
+ public isXsOrSm$: Observable;
+
+ public showAuth = Observable.of(false);
+
+ public user: Observable;
+
+ constructor(private store: Store,
+ private windowService: HostWindowService) {
+ this.isXsOrSm$ = this.windowService.isXsOrSm();
+ }
+
+ ngOnInit(): void {
+ // set isAuthenticated
+ this.isAuthenticated = this.store.select(isAuthenticated);
+
+ // set loading
+ this.loading = this.store.select(isAuthenticationLoading);
+
+ this.user = this.store.select(getAuthenticatedUser);
+
+ this.showAuth = this.store.select(routerStateSelector)
+ .filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state))
+ .map((router: RouterReducerState) => {
+ return !router.state.url.startsWith(LOGIN_ROUTE) && !router.state.url.startsWith(LOGOUT_ROUTE);
+ });
+ }
+}
diff --git a/src/app/shared/host-window.service.ts b/src/app/shared/host-window.service.ts
index 13ecbe7538..ecbee685f1 100644
--- a/src/app/shared/host-window.service.ts
+++ b/src/app/shared/host-window.service.ts
@@ -92,4 +92,12 @@ export class HostWindowService {
distinctUntilChanged()
);
}
+
+ isXsOrSm(): Observable {
+ return Observable.combineLatest(
+ this.isXs(),
+ this.isSm(),
+ ((isXs, isSm) => isXs || isSm)
+ ).distinctUntilChanged();
+ }
}
diff --git a/src/app/shared/log-in/log-in.component.html b/src/app/shared/log-in/log-in.component.html
new file mode 100644
index 0000000000..fe9a506e71
--- /dev/null
+++ b/src/app/shared/log-in/log-in.component.html
@@ -0,0 +1,28 @@
+
+
+
+
diff --git a/src/app/shared/log-in/log-in.component.scss b/src/app/shared/log-in/log-in.component.scss
new file mode 100644
index 0000000000..5e4393edaf
--- /dev/null
+++ b/src/app/shared/log-in/log-in.component.scss
@@ -0,0 +1,15 @@
+@import '../../../styles/variables.scss';
+
+.form-login .form-control:focus {
+ z-index: 2;
+}
+.form-login input[type="email"] {
+ margin-bottom: -1px;
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+}
+.form-login input[type="password"] {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
diff --git a/src/app/shared/log-in/log-in.component.spec.ts b/src/app/shared/log-in/log-in.component.spec.ts
new file mode 100644
index 0000000000..dc4a0be1c6
--- /dev/null
+++ b/src/app/shared/log-in/log-in.component.spec.ts
@@ -0,0 +1,127 @@
+import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
+import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
+
+import { By } from '@angular/platform-browser';
+import { Store, StoreModule } from '@ngrx/store';
+
+import { LogInComponent } from './log-in.component';
+import { authReducer } from '../../core/auth/auth.reducer';
+import { EpersonMock } from '../testing/eperson-mock';
+import { Eperson } from '../../core/eperson/models/eperson.model';
+import { TranslateModule } from '@ngx-translate/core';
+import { AuthService } from '../../core/auth/auth.service';
+import { AuthServiceStub } from '../testing/auth-service-stub';
+import { AppState } from '../../app.reducer';
+
+describe('LogInComponent', () => {
+
+ let component: LogInComponent;
+ let fixture: ComponentFixture;
+ let page: Page;
+ let user: Eperson;
+
+ const authState = {
+ authenticated: false,
+ loaded: false,
+ loading: false,
+ };
+
+ beforeEach(() => {
+ user = EpersonMock;
+ });
+
+ beforeEach(async(() => {
+ // refine the test module by declaring the test component
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ ReactiveFormsModule,
+ StoreModule.forRoot(authReducer),
+ TranslateModule.forRoot()
+ ],
+ declarations: [
+ LogInComponent
+ ],
+ providers: [
+ {provide: AuthService, useClass: AuthServiceStub}
+ ],
+ schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ })
+ .compileComponents();
+
+ }));
+
+ beforeEach(inject([Store], (store: Store) => {
+ store
+ .subscribe((state) => {
+ (state as any).core = Object.create({});
+ (state as any).core.auth = authState;
+ });
+
+ // create component and test fixture
+ fixture = TestBed.createComponent(LogInComponent);
+
+ // get test component from the fixture
+ component = fixture.componentInstance;
+
+ // create page
+ page = new Page(component, fixture);
+
+ // verify the fixture is stable (no pending tasks)
+ fixture.whenStable().then(() => {
+ page.addPageElements();
+ });
+
+ }));
+
+ it('should create a FormGroup comprised of FormControls', () => {
+ fixture.detectChanges();
+ expect(component.form instanceof FormGroup).toBe(true);
+ });
+
+ it('should authenticate', () => {
+ fixture.detectChanges();
+
+ // set FormControl values
+ component.form.controls.email.setValue('user');
+ component.form.controls.password.setValue('password');
+
+ // submit form
+ component.submit();
+
+ // verify Store.dispatch() is invoked
+ expect(page.navigateSpy.calls.any()).toBe(true, 'Store.dispatch not invoked');
+ });
+});
+
+/**
+ * I represent the DOM elements and attach spies.
+ *
+ * @class Page
+ */
+class Page {
+
+ public emailInput: HTMLInputElement;
+ public navigateSpy: jasmine.Spy;
+ public passwordInput: HTMLInputElement;
+
+ constructor(private component: LogInComponent, private fixture: ComponentFixture) {
+ // use injector to get services
+ const injector = fixture.debugElement.injector;
+ const store = injector.get(Store);
+
+ // add spies
+ this.navigateSpy = spyOn(store, 'dispatch');
+ }
+
+ public addPageElements() {
+ const emailInputSelector = 'input[formcontrolname=\'email\']';
+ this.emailInput = this.fixture.debugElement.query(By.css(emailInputSelector)).nativeElement;
+
+ const passwordInputSelector = 'input[formcontrolname=\'password\']';
+ this.passwordInput = this.fixture.debugElement.query(By.css(passwordInputSelector)).nativeElement;
+ }
+}
diff --git a/src/app/shared/log-in/log-in.component.ts b/src/app/shared/log-in/log-in.component.ts
new file mode 100644
index 0000000000..3364b1067d
--- /dev/null
+++ b/src/app/shared/log-in/log-in.component.ts
@@ -0,0 +1,184 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+
+import { Store } from '@ngrx/store';
+import { Observable } from 'rxjs/Observable';
+import 'rxjs/add/operator/filter';
+import 'rxjs/add/operator/takeWhile';
+
+import { AuthenticateAction, ResetAuthenticationMessagesAction } from '../../core/auth/auth.actions';
+
+import {
+ getAuthenticationError,
+ getAuthenticationInfo,
+ isAuthenticated,
+ isAuthenticationLoading,
+} from '../../core/auth/selectors';
+import { CoreState } from '../../core/core.reducers';
+
+import { isNotEmpty } from '../empty.util';
+import { fadeOut } from '../animations/fade';
+import { AuthService } from '../../core/auth/auth.service';
+
+/**
+ * /users/sign-in
+ * @class LogInComponent
+ */
+@Component({
+ selector: 'ds-log-in',
+ templateUrl: './log-in.component.html',
+ styleUrls: ['./log-in.component.scss'],
+ animations: [fadeOut]
+})
+export class LogInComponent implements OnDestroy, OnInit {
+
+ /**
+ * The error if authentication fails.
+ * @type {Observable}
+ */
+ public error: Observable;
+
+ /**
+ * Has authentication error.
+ * @type {boolean}
+ */
+ public hasError = false;
+
+ /**
+ * The authentication info message.
+ * @type {Observable}
+ */
+ public message: Observable;
+
+ /**
+ * Has authentication message.
+ * @type {boolean}
+ */
+ public hasMessage = false;
+
+ /**
+ * Whether user is authenticated.
+ * @type {Observable}
+ */
+ public isAuthenticated: Observable;
+
+ /**
+ * True if the authentication is loading.
+ * @type {boolean}
+ */
+ public loading: Observable;
+
+ /**
+ * The authentication form.
+ * @type {FormGroup}
+ */
+ public form: FormGroup;
+
+ /**
+ * Component state.
+ * @type {boolean}
+ */
+ private alive = true;
+
+ /**
+ * @constructor
+ * @param {AuthService} authService
+ * @param {FormBuilder} formBuilder
+ * @param {Store} store
+ */
+ constructor(
+ private authService: AuthService,
+ private formBuilder: FormBuilder,
+ private store: Store
+ ) {
+ }
+
+ /**
+ * Lifecycle hook that is called after data-bound properties of a directive are initialized.
+ * @method ngOnInit
+ */
+ public ngOnInit() {
+ // set isAuthenticated
+ this.isAuthenticated = this.store.select(isAuthenticated);
+
+ // set formGroup
+ this.form = this.formBuilder.group({
+ email: ['', Validators.required],
+ password: ['', Validators.required]
+ });
+
+ // set error
+ this.error = this.store.select(getAuthenticationError)
+ .map((error) => {
+ this.hasError = (isNotEmpty(error));
+ return error;
+ });
+
+ // set error
+ this.message = this.store.select(getAuthenticationInfo)
+ .map((message) => {
+ this.hasMessage = (isNotEmpty(message));
+ return message;
+ });
+
+ // set loading
+ this.loading = this.store.select(isAuthenticationLoading);
+
+ // subscribe to success
+ this.store.select(isAuthenticated)
+ .takeWhile(() => this.alive)
+ .filter((authenticated) => authenticated)
+ .subscribe(() => {
+ this.authService.redirectToPreviousUrl();
+ });
+ }
+
+ /**
+ * Lifecycle hook that is called when a directive, pipe or service is destroyed.
+ * @method ngOnDestroy
+ */
+ public ngOnDestroy() {
+ this.alive = false;
+ }
+
+ /**
+ * Reset error or message.
+ */
+ public resetErrorOrMessage() {
+ if (this.hasError || this.hasMessage) {
+ this.store.dispatch(new ResetAuthenticationMessagesAction());
+ this.hasError = false;
+ this.hasMessage = false;
+ }
+ }
+
+ /**
+ * To the registration page.
+ * @method register
+ */
+ public register() {
+ // TODO enable after registration process is done
+ // this.router.navigate(['/register']);
+ }
+
+ /**
+ * Submit the authentication form.
+ * @method submit
+ */
+ public submit() {
+ this.resetErrorOrMessage();
+ // get email and password values
+ const email: string = this.form.get('email').value;
+ const password: string = this.form.get('password').value;
+
+ // trim values
+ email.trim();
+ password.trim();
+
+ // dispatch AuthenticationAction
+ this.store.dispatch(new AuthenticateAction(email, password));
+
+ // clear form
+ this.form.reset();
+ }
+}
diff --git a/src/app/shared/log-out/log-out.component.html b/src/app/shared/log-out/log-out.component.html
new file mode 100644
index 0000000000..f3ceae0087
--- /dev/null
+++ b/src/app/shared/log-out/log-out.component.html
@@ -0,0 +1,7 @@
+
+
diff --git a/src/app/shared/log-out/log-out.component.scss b/src/app/shared/log-out/log-out.component.scss
new file mode 100644
index 0000000000..dcd67e092f
--- /dev/null
+++ b/src/app/shared/log-out/log-out.component.scss
@@ -0,0 +1 @@
+@import '../log-in/log-in.component.scss';
diff --git a/src/app/shared/log-out/log-out.component.spec.ts b/src/app/shared/log-out/log-out.component.spec.ts
new file mode 100644
index 0000000000..ad609f0aea
--- /dev/null
+++ b/src/app/shared/log-out/log-out.component.spec.ts
@@ -0,0 +1,108 @@
+import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+
+import { Store, StoreModule } from '@ngrx/store';
+
+import { authReducer } from '../../core/auth/auth.reducer';
+import { EpersonMock } from '../testing/eperson-mock';
+import { Eperson } from '../../core/eperson/models/eperson.model';
+import { TranslateModule } from '@ngx-translate/core';
+import { Router } from '@angular/router';
+import { AppState } from '../../app.reducer';
+import { LogOutComponent } from './log-out.component';
+import { RouterStub } from '../testing/router-stub';
+
+describe('LogOutComponent', () => {
+
+ let component: LogOutComponent;
+ let fixture: ComponentFixture;
+ let page: Page;
+ let user: Eperson;
+
+ const authState = {
+ authenticated: false,
+ loaded: false,
+ loading: false,
+ };
+ const routerStub = new RouterStub();
+
+ beforeEach(() => {
+ user = EpersonMock;
+ });
+
+ beforeEach(async(() => {
+ // refine the test module by declaring the test component
+ TestBed.configureTestingModule({
+ imports: [
+ FormsModule,
+ ReactiveFormsModule,
+ StoreModule.forRoot(authReducer),
+ TranslateModule.forRoot()
+ ],
+ declarations: [
+ LogOutComponent
+ ],
+ providers: [
+ {provide: Router, useValue: routerStub},
+ ],
+ schemas: [
+ CUSTOM_ELEMENTS_SCHEMA
+ ]
+ })
+ .compileComponents();
+
+ }));
+
+ beforeEach(inject([Store], (store: Store) => {
+ store
+ .subscribe((state) => {
+ (state as any).core = Object.create({});
+ (state as any).core.auth = authState;
+ });
+
+ // create component and test fixture
+ fixture = TestBed.createComponent(LogOutComponent);
+
+ // get test component from the fixture
+ component = fixture.componentInstance;
+
+ // create page
+ page = new Page(component, fixture);
+
+ }));
+
+ it('should create an instance', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should log out', () => {
+ fixture.detectChanges();
+
+ // submit form
+ component.logOut();
+
+ // verify Store.dispatch() is invoked
+ expect(page.navigateSpy.calls.any()).toBe(true, 'Store.dispatch not invoked');
+ });
+});
+
+/**
+ * I represent the DOM elements and attach spies.
+ *
+ * @class Page
+ */
+class Page {
+
+ public navigateSpy: jasmine.Spy;
+
+ constructor(private component: LogOutComponent, private fixture: ComponentFixture) {
+ // use injector to get services
+ const injector = fixture.debugElement.injector;
+ const store = injector.get(Store);
+
+ // add spies
+ this.navigateSpy = spyOn(store, 'dispatch');
+ }
+
+}
diff --git a/src/app/shared/log-out/log-out.component.ts b/src/app/shared/log-out/log-out.component.ts
new file mode 100644
index 0000000000..37d0b142f9
--- /dev/null
+++ b/src/app/shared/log-out/log-out.component.ts
@@ -0,0 +1,82 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+
+// @ngrx
+import { Store } from '@ngrx/store';
+
+// actions
+import { LogOutAction } from '../../core/auth/auth.actions';
+
+// reducers
+import {
+ getLogOutError,
+ isAuthenticated,
+ isAuthenticationLoading,
+} from '../../core/auth/selectors';
+
+import { AppState } from '../../app.reducer';
+import { Observable } from 'rxjs/Observable';
+import { fadeOut } from '../animations/fade';
+
+@Component({
+ selector: 'ds-log-out',
+ templateUrl: './log-out.component.html',
+ styleUrls: ['./log-out.component.scss'],
+ animations: [fadeOut]
+})
+export class LogOutComponent implements OnDestroy, OnInit {
+ /**
+ * The error if authentication fails.
+ * @type {Observable}
+ */
+ public error: Observable;
+
+ /**
+ * True if the logout is loading.
+ * @type {boolean}
+ */
+ public loading: Observable;
+
+ /**
+ * Component state.
+ * @type {boolean}
+ */
+ private alive = true;
+
+ /**
+ * @constructor
+ * @param {Store} store
+ */
+ constructor(private router: Router,
+ private store: Store) { }
+
+ /**
+ * Lifecycle hook that is called when a directive, pipe or service is destroyed.
+ */
+ public ngOnDestroy() {
+ this.alive = false;
+ }
+
+ /**
+ * Lifecycle hook that is called after data-bound properties of a directive are initialized.
+ */
+ ngOnInit() {
+ // set error
+ this.error = this.store.select(getLogOutError);
+
+ // set loading
+ this.loading = this.store.select(isAuthenticationLoading);
+ }
+
+ /**
+ * Go to the home page.
+ */
+ public home() {
+ this.router.navigate(['/home']);
+ }
+
+ public logOut() {
+ this.store.dispatch(new LogOutAction());
+ }
+
+}
diff --git a/src/app/shared/mocks/mock-auth.service.ts b/src/app/shared/mocks/mock-auth.service.ts
new file mode 100644
index 0000000000..6258e4aa21
--- /dev/null
+++ b/src/app/shared/mocks/mock-auth.service.ts
@@ -0,0 +1,6 @@
+/* tslint:disable:no-empty */
+export class AuthServiceMock {
+ public checksAuthenticationToken() {
+ return
+ }
+}
diff --git a/src/app/shared/pagination/pagination.component.ts b/src/app/shared/pagination/pagination.component.ts
index faaf20ec79..3a1ba34e0c 100644
--- a/src/app/shared/pagination/pagination.component.ts
+++ b/src/app/shared/pagination/pagination.component.ts
@@ -207,8 +207,10 @@ export class PaginationComponent implements OnDestroy, OnInit {
this.pageSizeOptions = this.paginationOptions.pageSizeOptions;
this.currentPage = this.paginationOptions.currentPage;
this.pageSize = this.paginationOptions.pageSize;
- this.sortDirection = this.sortOptions.direction;
- this.sortField = this.sortOptions.field;
+ if (this.sortOptions) {
+ this.sortDirection = this.sortOptions.direction;
+ this.sortField = this.sortOptions.field;
+ }
this.currentQueryParams = {
pageId: this.id,
page: this.currentPage,
diff --git a/src/app/shared/api.service.ts b/src/app/shared/services/api.service.ts
similarity index 100%
rename from src/app/shared/api.service.ts
rename to src/app/shared/services/api.service.ts
diff --git a/src/app/shared/services/client-cookie.service.ts b/src/app/shared/services/client-cookie.service.ts
new file mode 100644
index 0000000000..4aa670ca78
--- /dev/null
+++ b/src/app/shared/services/client-cookie.service.ts
@@ -0,0 +1,25 @@
+import { Injectable } from '@angular/core'
+import { CookieAttributes, getJSON, remove, set } from 'js-cookie'
+import { CookieService, ICookieService } from './cookie.service';
+
+@Injectable()
+export class ClientCookieService extends CookieService implements ICookieService {
+
+ public set(name: string, value: any, options?: CookieAttributes): void {
+ set(name, value, options);
+ this.updateSource()
+ }
+
+ public remove(name: string, options?: CookieAttributes): void {
+ remove(name, options);
+ this.updateSource()
+ }
+
+ public get(name: string): any {
+ return getJSON(name)
+ }
+
+ public getAll(): any {
+ return getJSON()
+ }
+}
diff --git a/src/app/shared/services/cookie.service.spec.ts b/src/app/shared/services/cookie.service.spec.ts
new file mode 100644
index 0000000000..dddb8f095f
--- /dev/null
+++ b/src/app/shared/services/cookie.service.spec.ts
@@ -0,0 +1,28 @@
+import { CookieService, ICookieService } from './cookie.service'
+import { async, TestBed } from '@angular/core/testing'
+import { REQUEST } from '@nguniversal/express-engine/tokens'
+
+describe(CookieService.name, () => {
+ let service: ICookieService;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ CookieService,
+ {provide: REQUEST, useValue: {}}
+ ]
+ })
+ }));
+
+ beforeEach(() => {
+ service = TestBed.get(CookieService)
+ });
+
+ afterEach(() => {
+ TestBed.resetTestingModule()
+ });
+
+ it('should construct', async(() => {
+ expect(service).toBeDefined()
+ }))
+});
diff --git a/src/app/shared/services/cookie.service.ts b/src/app/shared/services/cookie.service.ts
new file mode 100644
index 0000000000..8265651d81
--- /dev/null
+++ b/src/app/shared/services/cookie.service.ts
@@ -0,0 +1,40 @@
+import { Inject, Injectable } from '@angular/core'
+
+import { REQUEST } from '@nguniversal/express-engine/tokens'
+
+import { Subject } from 'rxjs/Subject'
+import { Observable } from 'rxjs/Observable'
+import { CookieAttributes } from 'js-cookie'
+
+export interface ICookieService {
+ readonly cookies$: Observable<{ readonly [key: string]: any }>
+
+ getAll(): any
+
+ get(name: string): any
+
+ set(name: string, value: any, options?: CookieAttributes): void
+
+ remove(name: string, options?: CookieAttributes): void
+}
+
+@Injectable()
+export abstract class CookieService implements ICookieService {
+ protected readonly cookieSource = new Subject<{ readonly [key: string]: any }>();
+ public readonly cookies$ = this.cookieSource.asObservable();
+
+ constructor(@Inject(REQUEST) protected req: any) {
+ }
+
+ public abstract set(name: string, value: any, options?: CookieAttributes): void
+
+ public abstract remove(name: string, options?: CookieAttributes): void
+
+ public abstract get(name: string): any
+
+ public abstract getAll(): any
+
+ protected updateSource() {
+ this.cookieSource.next(this.getAll());
+ }
+}
diff --git a/src/app/shared/route.service.spec.ts b/src/app/shared/services/route.service.spec.ts
similarity index 100%
rename from src/app/shared/route.service.spec.ts
rename to src/app/shared/services/route.service.spec.ts
diff --git a/src/app/shared/route.service.ts b/src/app/shared/services/route.service.ts
similarity index 96%
rename from src/app/shared/route.service.ts
rename to src/app/shared/services/route.service.ts
index 9c2b64ede1..aa683a6403 100644
--- a/src/app/shared/route.service.ts
+++ b/src/app/shared/services/route.service.ts
@@ -4,7 +4,7 @@ import {
ActivatedRoute, convertToParamMap, NavigationExtras, Params,
Router,
} from '@angular/router';
-import { isNotEmpty } from './empty.util';
+import { isNotEmpty } from '../empty.util';
@Injectable()
export class RouteService {
diff --git a/src/app/shared/services/server-cookie.service.ts b/src/app/shared/services/server-cookie.service.ts
new file mode 100644
index 0000000000..49cc738346
--- /dev/null
+++ b/src/app/shared/services/server-cookie.service.ts
@@ -0,0 +1,29 @@
+import { Injectable } from '@angular/core'
+import { CookieAttributes } from 'js-cookie'
+import { CookieService, ICookieService } from './cookie.service';
+
+@Injectable()
+export class ServerCookieService extends CookieService implements ICookieService {
+
+ public set(name: string, value: any, options?: CookieAttributes): void {
+ return
+ }
+
+ public remove(name: string, options?: CookieAttributes): void {
+ return
+ }
+
+ public get(name: string): any {
+ try {
+ return JSON.parse(this.req.cookies[name])
+ } catch (err) {
+ return this.req ? this.req.cookies[name] : undefined
+ }
+ }
+
+ public getAll(): any {
+ if (this.req) {
+ return this.req.cookies
+ }
+ }
+}
diff --git a/src/app/shared/server-response.service.ts b/src/app/shared/services/server-response.service.ts
similarity index 100%
rename from src/app/shared/server-response.service.ts
rename to src/app/shared/services/server-response.service.ts
diff --git a/src/app/shared/window.service.ts b/src/app/shared/services/window.service.ts
similarity index 100%
rename from src/app/shared/window.service.ts
rename to src/app/shared/services/window.service.ts
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index dff55ebb5c..57ba7dec4d 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -44,6 +44,9 @@ import { SearchResultGridElementComponent } from './object-grid/search-result-gr
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component';
import { VarDirective } from './utils/var.directive';
+import { LogInComponent } from './log-in/log-in.component';
+import { AuthNavMenuComponent } from './auth-nav-menu/auth-nav-menu.component';
+import { LogOutComponent } from './log-out/log-out.component';
import { FormComponent } from './form/form.component';
import { DsDynamicTypeaheadComponent } from './form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component';
import { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
@@ -101,6 +104,7 @@ const PIPES = [
const COMPONENTS = [
// put shared components here
+ AuthNavMenuComponent,
ChipsComponent,
ComcolPageContentComponent,
ComcolPageHeaderComponent,
@@ -117,6 +121,8 @@ const COMPONENTS = [
ErrorComponent,
FormComponent,
LoadingComponent,
+ LogInComponent,
+ LogOutComponent,
NumberPickerComponent,
ObjectListComponent,
AbstractListableElementComponent,
diff --git a/src/app/shared/testing/auth-request-service-stub.ts b/src/app/shared/testing/auth-request-service-stub.ts
new file mode 100644
index 0000000000..2c47068af4
--- /dev/null
+++ b/src/app/shared/testing/auth-request-service-stub.ts
@@ -0,0 +1,69 @@
+import { Observable } from 'rxjs/Observable';
+import { HttpOptions } from '../../core/dspace-rest-v2/dspace-rest-v2.service';
+import { AuthStatus } from '../../core/auth/models/auth-status.model';
+import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
+import { Eperson } from '../../core/eperson/models/eperson.model';
+import { isNotEmpty } from '../empty.util';
+import { EpersonMock } from './eperson-mock';
+
+export class AuthRequestServiceStub {
+ protected mockUser: Eperson = EpersonMock;
+ protected mockTokenInfo = new AuthTokenInfo('test_token');
+
+ public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable {
+ const authStatusStub: AuthStatus = new AuthStatus();
+ if (isNotEmpty(body)) {
+ const parsedBody = this.parseQueryString(body);
+ authStatusStub.okay = true;
+ if (parsedBody.user === 'user' && parsedBody.password === 'password') {
+ authStatusStub.authenticated = true;
+ authStatusStub.token = this.mockTokenInfo;
+ } else {
+ authStatusStub.authenticated = false;
+ }
+ } else {
+ const token = (options.headers as any).lazyUpdate[1].value;
+ if (this.validateToken(token)) {
+ authStatusStub.authenticated = true;
+ authStatusStub.token = this.mockTokenInfo;
+ authStatusStub.eperson = [this.mockUser];
+ } else {
+ authStatusStub.authenticated = false;
+ }
+ }
+ return Observable.of(authStatusStub);
+ }
+
+ public getRequest(method: string, options?: HttpOptions): Observable {
+ const authStatusStub: AuthStatus = new AuthStatus();
+ switch (method) {
+ case 'logout':
+ authStatusStub.authenticated = false;
+ break;
+ case 'status':
+ const token = (options.headers as any).lazyUpdate[1].value;
+ if (this.validateToken(token)) {
+ authStatusStub.authenticated = true;
+ authStatusStub.token = this.mockTokenInfo;
+ authStatusStub.eperson = [this.mockUser];
+ } else {
+ authStatusStub.authenticated = false;
+ }
+ break;
+ }
+ return Observable.of(authStatusStub);
+ }
+
+ private validateToken(token): boolean {
+ return (token === 'Bearer test_token');
+ }
+ private parseQueryString(query): any {
+ const obj = Object.create({});
+ const vars = query.split('&');
+ for (const param of vars) {
+ const pair = param.split('=');
+ obj[pair[0]] = pair[1]
+ }
+ return obj;
+ }
+}
diff --git a/src/app/shared/testing/auth-service-stub.ts b/src/app/shared/testing/auth-service-stub.ts
new file mode 100644
index 0000000000..c7d5556910
--- /dev/null
+++ b/src/app/shared/testing/auth-service-stub.ts
@@ -0,0 +1,95 @@
+import { AuthStatus } from '../../core/auth/models/auth-status.model';
+import { Observable } from 'rxjs/Observable';
+import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
+import { EpersonMock } from './eperson-mock';
+import { Eperson } from '../../core/eperson/models/eperson.model';
+
+export class AuthServiceStub {
+
+ token: AuthTokenInfo = new AuthTokenInfo('token_test');
+ private _tokenExpired = false;
+
+ constructor() {
+ this.token.expires = Date.now() + (1000 * 60 * 60);
+ }
+
+ public authenticate(user: string, password: string): Observable {
+ if (user === 'user' && password === 'password') {
+ const authStatus = new AuthStatus();
+ authStatus.okay = true;
+ authStatus.authenticated = true;
+ authStatus.token = this.token;
+ authStatus.eperson = [EpersonMock];
+ return Observable.of(authStatus);
+ } else {
+ console.log('error');
+ throw(new Error('Message Error test'));
+ }
+ }
+
+ public authenticatedUser(token: AuthTokenInfo): Observable {
+ if (token.accessToken === 'token_test') {
+ return Observable.of(EpersonMock);
+ } else {
+ throw(new Error('Message Error test'));
+ }
+ }
+
+ public buildAuthHeader(token?: AuthTokenInfo): string {
+ return `Bearer ${token.accessToken}`;
+ }
+
+ public getToken(): AuthTokenInfo {
+ return this.token;
+ }
+
+ public hasValidAuthenticationToken(): Observable {
+ return Observable.of(this.token);
+ }
+
+ public logout(): Observable {
+ return Observable.of(true);
+ }
+
+ public isTokenExpired(token?: AuthTokenInfo): boolean {
+ return this._tokenExpired;
+ }
+
+ /**
+ * This method is used to ease testing
+ */
+ public setTokenAsExpired() {
+ this._tokenExpired = true
+ }
+
+ /**
+ * This method is used to ease testing
+ */
+ public setTokenAsNotExpired() {
+ this._tokenExpired = false
+ }
+
+ public isTokenExpiring(): Observable {
+ return Observable.of(false);
+ }
+
+ public refreshAuthenticationToken(token: AuthTokenInfo): Observable {
+ return Observable.of(this.token);
+ }
+
+ public redirectToPreviousUrl() {
+ return;
+ }
+
+ public removeToken() {
+ return;
+ }
+
+ setRedirectUrl(url: string) {
+ return;
+ }
+
+ public storeToken(token: AuthTokenInfo) {
+ return;
+ }
+}
diff --git a/src/app/shared/testing/eperson-mock.ts b/src/app/shared/testing/eperson-mock.ts
new file mode 100644
index 0000000000..9cf938fcf2
--- /dev/null
+++ b/src/app/shared/testing/eperson-mock.ts
@@ -0,0 +1,34 @@
+import { Eperson } from '../../core/eperson/models/eperson.model';
+
+export const EpersonMock: Eperson = Object.assign(new Eperson(),{
+ handle: null,
+ groups: [],
+ netid: 'test@test.com',
+ lastActive: '2018-05-14T12:25:42.411+0000',
+ canLogIn: true,
+ email: 'test@test.com',
+ requireCertificate: false,
+ selfRegistered: false,
+ self: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons/testid',
+ id: 'testid',
+ uuid: 'testid',
+ type: 'eperson',
+ name: 'User Test',
+ metadata: [
+ {
+ key: 'eperson.firstname',
+ language: null,
+ value: 'User'
+ },
+ {
+ key: 'eperson.lastname',
+ language: null,
+ value: 'Test'
+ },
+ {
+ key: 'eperson.language',
+ language: null,
+ value: 'en'
+ }
+ ]
+});
diff --git a/src/app/shared/testing/host-window-service-stub.ts b/src/app/shared/testing/host-window-service-stub.ts
index 98af7fda18..2833415477 100644
--- a/src/app/shared/testing/host-window-service-stub.ts
+++ b/src/app/shared/testing/host-window-service-stub.ts
@@ -16,4 +16,8 @@ export class HostWindowServiceStub {
isXs(): Observable {
return Observable.of(this.width < 576);
}
+
+ isXsOrSm(): Observable {
+ return this.isXs();
+ }
}
diff --git a/src/app/shared/testing/platform-service-stub.ts b/src/app/shared/testing/platform-service-stub.ts
new file mode 100644
index 0000000000..39b9b5f4d3
--- /dev/null
+++ b/src/app/shared/testing/platform-service-stub.ts
@@ -0,0 +1,12 @@
+
+// declare a stub service
+export class PlatformServiceStub {
+
+ public get isBrowser(): boolean {
+ return true;
+ }
+
+ public get isServer(): boolean {
+ return false;
+ }
+}
diff --git a/src/app/shared/truncatable/truncatable-part/truncatable-part.component.scss b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.scss
index ac3641df39..133328fec1 100644
--- a/src/app/shared/truncatable/truncatable-part/truncatable-part.component.scss
+++ b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.scss
@@ -23,6 +23,7 @@
max-width: 150px;
height: $height;
background: linear-gradient(to right, rgba(255, 255, 255, 0), $body-bg 70%);
+ pointer-events: none;
}
}
}
@@ -60,4 +61,4 @@ $h4-factor: strip-unit($h4-font-size);
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/app/shared/utils/encode-decode.util.spec.ts b/src/app/shared/utils/encode-decode.util.spec.ts
new file mode 100644
index 0000000000..c3039c482e
--- /dev/null
+++ b/src/app/shared/utils/encode-decode.util.spec.ts
@@ -0,0 +1,10 @@
+import { Base64EncodeUrl } from './encode-decode.util';
+
+describe('Encode/Decode Utils', () => {
+ const strng = '+string+/=t-';
+ const encodedStrng = '%2Bstring%2B%2F%3Dt-';
+
+ it('should return encoded string', () => {
+ expect(Base64EncodeUrl(strng)).toBe(encodedStrng);
+ });
+});
diff --git a/src/app/shared/utils/encode-decode.util.ts b/src/app/shared/utils/encode-decode.util.ts
new file mode 100644
index 0000000000..e21034b7bd
--- /dev/null
+++ b/src/app/shared/utils/encode-decode.util.ts
@@ -0,0 +1,10 @@
+/**
+ * use this to make a Base64 encoded string URL friendly,
+ * i.e. '+' and '/' are replaced with special percent-encoded hexadecimal sequences
+ *
+ * @param {String} str the encoded string
+ * @returns {String} the URL friendly encoded String
+ */
+export function Base64EncodeUrl(str): string {
+ return str.replace(/\+/g, '%2B').replace(/\//g, '%2F').replace(/\=/g, '%3D');
+}
diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts
index 1112ebdb23..a7a59dc837 100644
--- a/src/modules/app/browser-app.module.ts
+++ b/src/modules/app/browser-app.module.ts
@@ -1,8 +1,9 @@
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
-import { BrowserModule } from '@angular/platform-browser';
+import { BrowserModule, makeStateKey, TransferState } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
+import { REQUEST } from '@nguniversal/express-engine/tokens';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
@@ -14,13 +15,22 @@ import { AppComponent } from '../../app/app.component';
import { AppModule } from '../../app/app.module';
import { DSpaceBrowserTransferStateModule } from '../transfer-state/dspace-browser-transfer-state.module';
import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service';
+import { ClientCookieService } from '../../app/shared/services/client-cookie.service';
+import { CookieService } from '../../app/shared/services/cookie.service';
+import { AuthService } from '../../app/core/auth/auth.service';
import { Angulartics2Module } from 'angulartics2';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
+export const REQ_KEY = makeStateKey('req');
+
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, 'assets/i18n/', '.json');
}
+export function getRequest(transferState: TransferState): any {
+ return transferState.get(REQ_KEY, {})
+}
+
@NgModule({
bootstrap: [AppComponent],
imports: [
@@ -48,6 +58,21 @@ export function createTranslateLoader(http: HttpClient) {
}),
AppModule
],
+ providers: [
+ {
+ provide: REQUEST,
+ useFactory: getRequest,
+ deps: [TransferState]
+ },
+ {
+ provide: AuthService,
+ useClass: AuthService
+ },
+ {
+ provide: CookieService,
+ useClass: ClientCookieService
+ }
+ ]
})
export class BrowserAppModule {
constructor(
diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts
index fac1b63ada..10285e75f5 100644
--- a/src/modules/app/server-app.module.ts
+++ b/src/modules/app/server-app.module.ts
@@ -15,6 +15,10 @@ import { DSpaceServerTransferStateModule } from '../transfer-state/dspace-server
import { DSpaceTransferState } from '../transfer-state/dspace-transfer-state.service';
import { TranslateUniversalLoader } from '../translate-universal-loader';
+import { CookieService } from '../../app/shared/services/cookie.service';
+import { ServerCookieService } from '../../app/shared/services/server-cookie.service';
+import { AuthService } from '../../app/core/auth/auth.service';
+import { ServerAuthService } from '../../app/core/auth/server-auth.service';
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
import { AngularticsMock } from '../../app/shared/mocks/mock-angulartics.service';
@@ -45,7 +49,15 @@ export function createTranslateLoader() {
AppModule
],
providers: [
- { provide: Angulartics2GoogleAnalytics, useClass: AngularticsMock }
+ { provide: Angulartics2GoogleAnalytics, useClass: AngularticsMock },
+ {
+ provide: AuthService,
+ useClass: ServerAuthService
+ },
+ {
+ provide: CookieService,
+ useClass: ServerCookieService
+ }
]
})
export class ServerAppModule {
diff --git a/src/routes.ts b/src/routes.ts
index f051b16198..392d3925a5 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -3,6 +3,8 @@ export const ROUTES: string[] = [
'items/:id',
'collections/:id',
'communities/:id',
+ 'login',
+ 'logout',
'search',
'**'
];
diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss
index 8372aa934d..f378c2b7c9 100644
--- a/src/styles/_custom_variables.scss
+++ b/src/styles/_custom_variables.scss
@@ -6,4 +6,6 @@ $card-thumbnail-height:240px;
$dropdown-menu-max-height: 200px;
$drop-zone-area-z-index: 1025;
$drop-zone-area-inner-z-index: 1021;
+$login-logo-height:72px;
+$login-logo-width:72px;
diff --git a/tsconfig.json b/tsconfig.json
index 8ab72a4327..8037c659ed 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -26,8 +26,7 @@
"es6",
"es2015",
"es2016",
- "es2017",
- "es2017.object"
+ "es2017"
]
},
"exclude": [
diff --git a/yarn.lock b/yarn.lock
index 8c2d39acd1..3207959415 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -215,6 +215,10 @@
version "2.8.6"
resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.8.6.tgz#14445b6a1613cf4e05dd61c3c3256d0e95c0421e"
+"@types/js-cookie@2.1.0":
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.1.0.tgz#a8916246aa994db646c66d54c854916213300a51"
+
"@types/lodash@4.14.74":
version "4.14.74"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.74.tgz#ac3bd8db988e7f7038e5d22bd76a7ba13f876168"
@@ -4498,6 +4502,10 @@ js-base64@^2.1.8, js-base64@^2.1.9:
version "2.3.2"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf"
+js-cookie@2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.0.tgz#1b2c279a6eece380a12168b92485265b35b1effb"
+
js-tokens@^3.0.0, js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
@@ -4619,6 +4627,10 @@ jszip@^3.1.3:
pako "~1.0.2"
readable-stream "~2.0.6"
+jwt-decode@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
+
karma-chrome-launcher@2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz#cf1b9d07136cc18fe239327d24654c3dbc368acf"