mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-09 19:13:08 +00:00
Merge branch 'w2p-72699_Hard-redirect-after-log-in' into w2p-72541_User-agreement-and-Privacy-statement
Conflicts: src/app/app-routing.module.ts src/app/core/core.module.ts
This commit is contained in:
5
.github/pull_request_template.md
vendored
5
.github/pull_request_template.md
vendored
@@ -21,8 +21,5 @@ _This checklist provides a reminder of what we are going to look for when review
|
||||
- [ ] My PR is small in size (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible.
|
||||
- [ ] My PR passes [TSLint](https://palantir.github.io/tslint/) validation using `yarn run lint`
|
||||
- [ ] My PR includes [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods.
|
||||
- [ ] My PR passes all specs/tests and includes new/updated specs for any bug fixes, improvements or new features. A few reminders about what constitutes good tests:
|
||||
* Include tests for different user types (if behavior differs), including: (1) Anonymous user, (2) Logged in user (non-admin), and (3) Administrator.
|
||||
* Include tests for error scenarios, e.g. when errors/warnings should appear (or buttons should be disabled).
|
||||
* For bug fixes, include a test that reproduces the bug and proves it is fixed. For clarity, it may be useful to provide the test in a separate commit from the bug fix.
|
||||
- [ ] My PR passes all specs/tests and includes new/updated specs or tests based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide).
|
||||
- [ ] If my PR includes new, third-party dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
|
||||
|
@@ -51,10 +51,10 @@ before_script:
|
||||
|
||||
script:
|
||||
# build app and run all tests
|
||||
- ng lint
|
||||
- travis_wait yarn run build:prod
|
||||
- yarn test:headless
|
||||
- yarn run e2e:ci
|
||||
- ng lint || travis_terminate 1;
|
||||
- travis_wait yarn run build:prod || travis_terminate 1;
|
||||
- yarn test:headless || travis_terminate 1;
|
||||
- yarn run e2e:ci || travis_terminate 1;
|
||||
|
||||
after_script:
|
||||
# Shutdown docker after everything runs
|
||||
|
@@ -18,7 +18,7 @@
|
||||
"builder": "@angular-builders/custom-webpack:browser",
|
||||
"options": {
|
||||
"customWebpackConfig": {
|
||||
"path": "./webpack/webpack.common.ts",
|
||||
"path": "./webpack/webpack.browser.ts",
|
||||
"mergeStrategies": {
|
||||
"loaders": "prepend"
|
||||
}
|
||||
@@ -30,7 +30,8 @@
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": false,
|
||||
"assets": [
|
||||
"src/assets"
|
||||
"src/assets",
|
||||
"src/robots.txt"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
@@ -84,7 +85,7 @@
|
||||
"builder": "@angular-builders/custom-webpack:karma",
|
||||
"options": {
|
||||
"customWebpackConfig": {
|
||||
"path": "./webpack/webpack.common.ts",
|
||||
"path": "./webpack/webpack.test.ts",
|
||||
"mergeStrategies": {
|
||||
"loaders": "prepend"
|
||||
}
|
||||
|
@@ -146,6 +146,7 @@
|
||||
"dotenv": "^8.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^0.4.10",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"http-proxy-middleware": "^1.0.5",
|
||||
"jasmine-core": "^3.3.0",
|
||||
"jasmine-marbles": "0.3.1",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
|
@@ -54,6 +54,13 @@ import(environmentFilePath)
|
||||
function generateEnvironmentFile(file: GlobalConfig): void {
|
||||
file.production = production;
|
||||
buildBaseUrls(file);
|
||||
|
||||
// TODO remove workaround in beta 5
|
||||
if (file.rest.nameSpace.match("(.*)/api/?$") !== null) {
|
||||
const newValue = getNameSpace(file.rest.nameSpace);
|
||||
console.log(colors.white.bgMagenta.bold(`The rest.nameSpace property in your environment file or in your DSPACE_REST_NAMESPACE environment variable ends with '/api'.\nThis is deprecated. As '/api' isn't configurable on the rest side, it shouldn't be repeated in every environment file.\nPlease change the rest nameSpace to '${newValue}'`));
|
||||
}
|
||||
|
||||
const contents = `export const environment = ` + JSON.stringify(file);
|
||||
writeFile(targetPath, contents, (err) => {
|
||||
if (err) {
|
||||
@@ -112,5 +119,16 @@ function getPort(port: number): string {
|
||||
}
|
||||
|
||||
function getNameSpace(nameSpace: string): string {
|
||||
// TODO remove workaround in beta 5
|
||||
const apiMatches = nameSpace.match("(.*)/api/?$");
|
||||
if (apiMatches != null) {
|
||||
let newValue = '/'
|
||||
if (hasValue(apiMatches[1])) {
|
||||
newValue = apiMatches[1];
|
||||
}
|
||||
return newValue;
|
||||
}
|
||||
else {
|
||||
return nameSpace ? nameSpace.charAt(0) === '/' ? nameSpace : '/' + nameSpace : '';
|
||||
}
|
||||
}
|
||||
|
@@ -33,6 +33,7 @@ import { enableProdMode, NgModuleFactory, Type } from '@angular/core';
|
||||
|
||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||
import { environment } from './src/environments/environment';
|
||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||
|
||||
/*
|
||||
* Set path for the browser application's dist folder
|
||||
@@ -106,6 +107,11 @@ app.set('view engine', 'html');
|
||||
*/
|
||||
app.set('views', DIST_FOLDER);
|
||||
|
||||
/**
|
||||
* Proxy the sitemaps
|
||||
*/
|
||||
app.use('/sitemap**', createProxyMiddleware({ target: `${environment.rest.baseUrl}/sitemaps`, changeOrigin: true }));
|
||||
|
||||
/*
|
||||
* Adds a cache control header to the response
|
||||
* The cache control value can be configured in the environments file and defaults to max-age=60
|
||||
|
@@ -0,0 +1,15 @@
|
||||
<div class="container">
|
||||
<h2 id="header">{{'admin.metadata-import.page.header' | translate}}</h2>
|
||||
<p>{{'admin.metadata-import.page.help' | translate}}</p>
|
||||
|
||||
<ds-file-dropzone-no-uploader
|
||||
(onFileAdded)="setFile($event)"
|
||||
[dropMessageLabel]="'admin.metadata-import.page.dropMsg'"
|
||||
[dropMessageLabelReplacement]="'admin.metadata-import.page.dropMsgReplace'">
|
||||
</ds-file-dropzone-no-uploader>
|
||||
|
||||
<button class="btn btn-secondary" id="backButton"
|
||||
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
|
||||
<button class="btn btn-primary" id="proceedButton"
|
||||
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
|
||||
</div>
|
@@ -0,0 +1,151 @@
|
||||
import { Location } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import {
|
||||
METADATA_IMPORT_SCRIPT_NAME,
|
||||
ScriptDataService
|
||||
} from '../../core/data/processes/script-data.service';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||
import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive';
|
||||
import { FileValidator } from '../../shared/utils/require-file.validator';
|
||||
import { MetadataImportPageComponent } from './metadata-import-page.component';
|
||||
|
||||
describe('MetadataImportPageComponent', () => {
|
||||
let comp: MetadataImportPageComponent;
|
||||
let fixture: ComponentFixture<MetadataImportPageComponent>;
|
||||
|
||||
let user;
|
||||
|
||||
let notificationService: NotificationsServiceStub;
|
||||
let scriptService: any;
|
||||
let router;
|
||||
let authService;
|
||||
let locationStub;
|
||||
|
||||
function init() {
|
||||
notificationService = new NotificationsServiceStub();
|
||||
scriptService = jasmine.createSpyObj('scriptService',
|
||||
{
|
||||
invoke: observableOf({
|
||||
response:
|
||||
{
|
||||
isSuccessful: true,
|
||||
resourceSelfLinks: ['https://localhost:8080/api/core/processes/45']
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
user = Object.assign(new EPerson(), {
|
||||
id: 'userId',
|
||||
email: 'user@test.com'
|
||||
});
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
getAuthenticatedUserFromStore: observableOf(user)
|
||||
});
|
||||
router = jasmine.createSpyObj('router', {
|
||||
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||
});
|
||||
locationStub = jasmine.createSpyObj('location', {
|
||||
back: jasmine.createSpy('back')
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
TranslateModule.forRoot(),
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [MetadataImportPageComponent, FileValueAccessorDirective, FileValidator],
|
||||
providers: [
|
||||
{ provide: NotificationsService, useValue: notificationService },
|
||||
{ provide: ScriptDataService, useValue: scriptService },
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: Location, useValue: locationStub },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MetadataImportPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(comp).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('if back button is pressed', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
const proceed = fixture.debugElement.query(By.css('#backButton')).nativeElement;
|
||||
proceed.click();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('should do location.back', () => {
|
||||
expect(locationStub.back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if file is set', () => {
|
||||
let fileMock: File;
|
||||
|
||||
beforeEach(() => {
|
||||
fileMock = new File([''], 'filename.txt', { type: 'text/plain' });
|
||||
comp.setFile(fileMock);
|
||||
});
|
||||
|
||||
describe('if proceed button is pressed', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||
proceed.click();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('metadata-import script is invoked with its -e currentUserEmail, -f fileName and the mockFile', () => {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '-e', value: user.email }),
|
||||
Object.assign(new ProcessParameter(), { name: '-f', value: 'filename.txt' }),
|
||||
];
|
||||
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
|
||||
});
|
||||
it('success notification is shown', () => {
|
||||
expect(notificationService.success).toHaveBeenCalled();
|
||||
});
|
||||
it('redirected to process page', () => {
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
|
||||
});
|
||||
});
|
||||
|
||||
describe('if proceed is pressed; but script invoke fails', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
jasmine.getEnv().allowRespy(true);
|
||||
spyOn(scriptService, 'invoke').and.returnValue(observableOf({
|
||||
response:
|
||||
{
|
||||
isSuccessful: false,
|
||||
}
|
||||
}));
|
||||
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||
proceed.click();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('error notification is shown', () => {
|
||||
expect(notificationService.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,106 @@
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||
import { RequestEntry } from '../../core/data/request.reducer';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-import-page',
|
||||
templateUrl: './metadata-import-page.component.html'
|
||||
})
|
||||
|
||||
/**
|
||||
* Component that represents a metadata import page for administrators
|
||||
*/
|
||||
export class MetadataImportPageComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The current value of the file
|
||||
*/
|
||||
fileObject: File;
|
||||
|
||||
/**
|
||||
* The authenticated user's email
|
||||
*/
|
||||
private currentUserEmail$: Observable<string>;
|
||||
|
||||
public constructor(protected authService: AuthService,
|
||||
private location: Location,
|
||||
protected translate: TranslateService,
|
||||
protected notificationsService: NotificationsService,
|
||||
private scriptDataService: ScriptDataService,
|
||||
private router: Router) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set file
|
||||
* @param file
|
||||
*/
|
||||
setFile(file) {
|
||||
this.fileObject = file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method provided by Angular. Invoked after the constructor.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.currentUserEmail$ = this.authService.getAuthenticatedUserFromStore().pipe(
|
||||
map((user: EPerson) => user.email)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* When return button is pressed go to previous location
|
||||
*/
|
||||
public onReturn() {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts import-metadata script with -e currentUserEmail -f fileName (and the selected file)
|
||||
*/
|
||||
public importMetadata() {
|
||||
if (this.fileObject == null) {
|
||||
this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile'));
|
||||
} else {
|
||||
this.currentUserEmail$.pipe(
|
||||
switchMap((email: string) => {
|
||||
if (isNotEmpty(email)) {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '-e', value: email }),
|
||||
Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }),
|
||||
];
|
||||
return this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject])
|
||||
.pipe(
|
||||
take(1),
|
||||
map((requestEntry: RequestEntry) => {
|
||||
if (requestEntry.response.isSuccessful) {
|
||||
const title = this.translate.get('process.new.notification.success.title');
|
||||
const content = this.translate.get('process.new.notification.success.content');
|
||||
this.notificationsService.success(title, content);
|
||||
const response: any = requestEntry.response;
|
||||
if (isNotEmpty(response.resourceSelfLinks)) {
|
||||
const processNumber = response.resourceSelfLinks[0].split('/').pop();
|
||||
this.router.navigateByUrl('/processes/' + processNumber);
|
||||
}
|
||||
} else {
|
||||
const title = this.translate.get('process.new.notification.error.title');
|
||||
const content = this.translate.get('process.new.notification.error.content');
|
||||
this.notificationsService.error(title, content);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}),
|
||||
take(1)
|
||||
).subscribe();
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { getAdminModulePath } from '../app-routing.module';
|
||||
import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component';
|
||||
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
|
||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
||||
@@ -48,6 +49,12 @@ export function getAccessControlModulePath() {
|
||||
component: AdminCurationTasksComponent,
|
||||
data: { title: 'admin.curation-tasks.title', breadcrumbKey: 'admin.curation-tasks' }
|
||||
},
|
||||
{
|
||||
path: 'metadata-import',
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
component: MetadataImportPageComponent,
|
||||
data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }
|
||||
},
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<div class="sidebar-collapsible">
|
||||
<span class="section-header-text">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(section.id); injector: itemInjectors.get(section.id);"></ng-container>
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
@@ -25,7 +25,7 @@
|
||||
|
||||
<ng-container *ngFor="let section of (sections | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="sectionComponents.get(section.id); injector: sectionInjectors.get(section.id);"></ng-container>
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</div>
|
||||
|
@@ -2,6 +2,7 @@ import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||
import { AdminSidebarComponent } from './admin-sidebar.component';
|
||||
import { MenuService } from '../../shared/menu/menu.service';
|
||||
import { MenuServiceStub } from '../../shared/testing/menu-service.stub';
|
||||
@@ -21,11 +22,13 @@ describe('AdminSidebarComponent', () => {
|
||||
let fixture: ComponentFixture<AdminSidebarComponent>;
|
||||
const menuService = new MenuServiceStub();
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let scriptService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true)
|
||||
});
|
||||
scriptService = jasmine.createSpyObj('scriptService', { scriptWithNameExistsAndCanExecute: observableOf(true) });
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule],
|
||||
declarations: [AdminSidebarComponent],
|
||||
@@ -36,9 +39,11 @@ describe('AdminSidebarComponent', () => {
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
{ provide: ActivatedRoute, useValue: {} },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: ScriptDataService, useValue: scriptService },
|
||||
{
|
||||
provide: NgbModal, useValue: {
|
||||
open: () => {/*comment*/}
|
||||
open: () => {/*comment*/
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@@ -1,9 +1,14 @@
|
||||
import { Component, Injector, OnInit } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { combineLatest as combineLatestObservable } from 'rxjs';
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
import { first, map, take } from 'rxjs/operators';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import {
|
||||
METADATA_EXPORT_SCRIPT_NAME,
|
||||
METADATA_IMPORT_SCRIPT_NAME,
|
||||
ScriptDataService
|
||||
} from '../../core/data/processes/script-data.service';
|
||||
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
|
||||
import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
||||
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||
@@ -11,6 +16,9 @@ import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/mod
|
||||
import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||
import {
|
||||
ExportMetadataSelectorComponent
|
||||
} from '../../shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
|
||||
import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state';
|
||||
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
|
||||
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
|
||||
@@ -64,7 +72,8 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
private variableService: CSSVariableService,
|
||||
private authService: AuthService,
|
||||
private modalService: NgbModal,
|
||||
private authorizationService: AuthorizationDataService
|
||||
private authorizationService: AuthorizationDataService,
|
||||
private scriptDataService: ScriptDataService,
|
||||
) {
|
||||
super(menuService, injector);
|
||||
}
|
||||
@@ -75,6 +84,8 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.createMenu();
|
||||
this.createSiteAdministratorMenuSections();
|
||||
this.createExportMenuSections();
|
||||
this.createImportMenuSections();
|
||||
super.ngOnInit();
|
||||
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
|
||||
this.authService.isAuthenticated()
|
||||
@@ -88,7 +99,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
this.sidebarOpen = !collapsed;
|
||||
this.sidebarClosed = collapsed;
|
||||
});
|
||||
this.sidebarExpanded = combineLatestObservable(this.menuCollapsed, this.menuPreviewCollapsed)
|
||||
this.sidebarExpanded = observableCombineLatest(this.menuCollapsed, this.menuPreviewCollapsed)
|
||||
.pipe(
|
||||
map(([collapsed, previewCollapsed]) => (!collapsed || !previewCollapsed))
|
||||
);
|
||||
@@ -225,94 +236,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
|
||||
/* Import */
|
||||
/* Curation tasks */
|
||||
{
|
||||
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',
|
||||
id: 'curation_tasks',
|
||||
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',
|
||||
text: 'menu.section.curation_task',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
icon: 'filter',
|
||||
index: 7
|
||||
},
|
||||
|
||||
/* Statistics */
|
||||
@@ -362,6 +297,146 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
|
||||
* the export scripts exist and the current user is allowed to execute them
|
||||
*/
|
||||
createExportMenuSections() {
|
||||
const menuList = [
|
||||
/* Export */
|
||||
{
|
||||
id: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.TEXT,
|
||||
text: 'menu.section.export'
|
||||
} as TextMenuItemModel,
|
||||
icon: 'sign-out-alt',
|
||||
index: 3,
|
||||
shouldPersistOnRouteChange: true
|
||||
},
|
||||
{
|
||||
id: 'export_community',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_community',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
},
|
||||
{
|
||||
id: 'export_collection',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_collection',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
},
|
||||
{
|
||||
id: 'export_item',
|
||||
parentID: 'export',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.export_item',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
},
|
||||
];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
||||
|
||||
observableCombineLatest(
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
// this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME)
|
||||
).pipe(
|
||||
// TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed; otherwise even in production mode, the metadata export button is only available after a refresh (and not in dev mode)
|
||||
// filter(([authorized, metadataExportScriptExists]: boolean[]) => authorized && metadataExportScriptExists),
|
||||
take(1)
|
||||
).subscribe(() => {
|
||||
this.menuService.addSection(this.menuID, {
|
||||
id: 'export_metadata',
|
||||
parentID: 'export',
|
||||
active: true,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.export_metadata',
|
||||
function: () => {
|
||||
this.modalService.open(ExportMetadataSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
|
||||
* the import scripts exist and the current user is allowed to execute them
|
||||
*/
|
||||
createImportMenuSections() {
|
||||
const menuList = [
|
||||
/* 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_batch',
|
||||
parentID: 'import',
|
||||
active: false,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.import_batch',
|
||||
link: ''
|
||||
} as LinkMenuItemModel,
|
||||
}
|
||||
];
|
||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
||||
shouldPersistOnRouteChange: true
|
||||
})));
|
||||
|
||||
observableCombineLatest(
|
||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
||||
// this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME)
|
||||
).pipe(
|
||||
// TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed
|
||||
// filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists),
|
||||
take(1)
|
||||
).subscribe(() => {
|
||||
this.menuService.addSection(this.menuID, {
|
||||
id: 'import_metadata',
|
||||
parentID: 'import',
|
||||
active: true,
|
||||
visible: true,
|
||||
model: {
|
||||
type: MenuItemType.LINK,
|
||||
text: 'menu.section.import_metadata',
|
||||
link: '/admin/metadata-import'
|
||||
} as LinkMenuItemModel,
|
||||
shouldPersistOnRouteChange: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create menu sections dependent on whether or not the current user is a site administrator
|
||||
*/
|
||||
|
@@ -12,15 +12,15 @@
|
||||
(click)="toggleSection($event)">
|
||||
<span class="section-header-text">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(section.id); injector: itemInjectors.get(section.id);"></ng-container>
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</span>
|
||||
<i class="fas fa-chevron-right fa-pull-right"
|
||||
[@rotate]="(expanded | async) ? 'expanded' : 'collapsed'" [title]="('menu.section.toggle.' + section.id) | translate"></i>
|
||||
</a>
|
||||
<ul class="sidebar-sub-level-items list-unstyled" @slide *ngIf="(expanded | async)">
|
||||
<li *ngFor="let subSection of (subSections | async)">
|
||||
<li *ngFor="let subSection of (subSections$ | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(subSection.id); injector: itemInjectors.get(subSection.id);"></ng-container>
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { AdminAccessControlModule } from './admin-access-control/admin-access-control.module';
|
||||
import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component';
|
||||
import { AdminRegistriesModule } from './admin-registries/admin-registries.module';
|
||||
import { AdminRoutingModule } from './admin-routing.module';
|
||||
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
|
||||
@@ -40,7 +41,9 @@ import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curati
|
||||
|
||||
WorkflowItemSearchResultAdminWorkflowListElementComponent,
|
||||
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
|
||||
WorkflowItemAdminWorkflowActionsComponent
|
||||
WorkflowItemAdminWorkflowActionsComponent,
|
||||
|
||||
MetadataImportPageComponent
|
||||
|
||||
],
|
||||
entryComponents: [
|
||||
@@ -54,7 +57,9 @@ import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curati
|
||||
|
||||
WorkflowItemSearchResultAdminWorkflowListElementComponent,
|
||||
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
|
||||
WorkflowItemAdminWorkflowActionsComponent
|
||||
WorkflowItemAdminWorkflowActionsComponent,
|
||||
|
||||
MetadataImportPageComponent
|
||||
]
|
||||
})
|
||||
export class AdminModule {
|
||||
|
@@ -1,5 +1,18 @@
|
||||
<ds-metadata-field-wrapper [label]="label | translate">
|
||||
<div class="file-section row" *ngFor="let file of (bitstreams$ | async); let last=last;">
|
||||
<div *ngVar="(originals$ | async)?.payload as originals">
|
||||
<h5 class="simple-view-element-header">{{"item.page.filesection.original.bundle" | translate}}</h5>
|
||||
<ds-pagination *ngIf="originals?.page?.length > 0"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
[paginationOptions]="originalOptions"
|
||||
[pageInfoState]="originals"
|
||||
[collectionSize]="originals?.totalElements"
|
||||
[disableRouteParameterUpdate]="true"
|
||||
(pageChange)="switchOriginalPage($event)">
|
||||
|
||||
|
||||
|
||||
<div class="file-section row" *ngFor="let file of originals?.page;">
|
||||
<div class="col-3">
|
||||
<ds-thumbnail [thumbnail]="(file.thumbnail | async)?.payload"></ds-thumbnail>
|
||||
</div>
|
||||
@@ -26,4 +39,49 @@
|
||||
</ds-file-download-link>
|
||||
</div>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
</div>
|
||||
|
||||
<div *ngVar="(licenses$ | async)?.payload as licenses">
|
||||
<h5 class="simple-view-element-header">{{"item.page.filesection.license.bundle" | translate}}</h5>
|
||||
<ds-pagination *ngIf="licenses?.page?.length > 0"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
[paginationOptions]="licenseOptions"
|
||||
[pageInfoState]="licenses"
|
||||
[collectionSize]="licenses?.totalElements"
|
||||
[disableRouteParameterUpdate]="true"
|
||||
(pageChange)="switchLicensePage($event)">
|
||||
|
||||
|
||||
|
||||
<div class="file-section row" *ngFor="let file of licenses?.page;">
|
||||
<div class="col-3">
|
||||
<ds-thumbnail [thumbnail]="(file.thumbnail | async)?.payload"></ds-thumbnail>
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<dl class="row">
|
||||
<dt class="col-md-4">{{"item.page.filesection.name" | translate}}</dt>
|
||||
<dd class="col-md-8">{{file.name}}</dd>
|
||||
|
||||
<dt class="col-md-4">{{"item.page.filesection.size" | translate}}</dt>
|
||||
<dd class="col-md-8">{{(file.sizeBytes) | dsFileSize }}</dd>
|
||||
|
||||
|
||||
<dt class="col-md-4">{{"item.page.filesection.format" | translate}}</dt>
|
||||
<dd class="col-md-8">{{(file.format | async)?.payload?.description}}</dd>
|
||||
|
||||
|
||||
<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
|
||||
<dd class="col-md-8">{{file.firstMetadataValue("dc.description")}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<ds-file-download-link [href]="file._links.content.href" [download]="file.name">
|
||||
{{"item.page.filesection.download" | translate}}
|
||||
</ds-file-download-link>
|
||||
</div>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
</div>
|
||||
</ds-metadata-field-wrapper>
|
||||
|
@@ -0,0 +1,117 @@
|
||||
import {FullFileSectionComponent} from './full-file-section.component';
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {createSuccessfulRemoteDataObject$} from '../../../../shared/remote-data.utils';
|
||||
import {createPaginatedList} from '../../../../shared/testing/utils.test';
|
||||
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
||||
import {TranslateLoaderMock} from '../../../../shared/mocks/translate-loader.mock';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {VarDirective} from '../../../../shared/utils/var.directive';
|
||||
import {FileSizePipe} from '../../../../shared/utils/file-size-pipe';
|
||||
import {MetadataFieldWrapperComponent} from '../../../field-components/metadata-field-wrapper/metadata-field-wrapper.component';
|
||||
import {BitstreamDataService} from '../../../../core/data/bitstream-data.service';
|
||||
import {NO_ERRORS_SCHEMA} from '@angular/core';
|
||||
import {Bitstream} from '../../../../core/shared/bitstream.model';
|
||||
import {of as observableOf} from 'rxjs';
|
||||
import {MockBitstreamFormat1} from '../../../../shared/mocks/item.mock';
|
||||
import {By} from '@angular/platform-browser';
|
||||
|
||||
describe('FullFileSectionComponent', () => {
|
||||
let comp: FullFileSectionComponent;
|
||||
let fixture: ComponentFixture<FullFileSectionComponent>;
|
||||
|
||||
const mockBitstream: Bitstream = Object.assign(new Bitstream(),
|
||||
{
|
||||
sizeBytes: 10201,
|
||||
content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||
format: observableOf(MockBitstreamFormat1),
|
||||
bundleName: 'ORIGINAL',
|
||||
_links: {
|
||||
self: {
|
||||
href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713'
|
||||
},
|
||||
content: {
|
||||
href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content'
|
||||
}
|
||||
},
|
||||
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
type: 'bitstream',
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: null,
|
||||
value: 'test_word.docx'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
||||
findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([mockBitstream, mockBitstream, mockBitstream]))
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}), BrowserAnimationsModule],
|
||||
declarations: [FullFileSectionComponent, VarDirective, FileSizePipe, MetadataFieldWrapperComponent],
|
||||
providers: [
|
||||
{provide: BitstreamDataService, useValue: bitstreamDataService}
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(FullFileSectionComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
describe('when the full file section gets loaded with bitstreams available', () => {
|
||||
it ('should contain a list with bitstreams', () => {
|
||||
const fileSection = fixture.debugElement.queryAll(By.css('.file-section'));
|
||||
expect(fileSection.length).toEqual(6);
|
||||
});
|
||||
|
||||
describe('when we press the pageChange button for original bundle', () => {
|
||||
beforeEach(() => {
|
||||
comp.switchOriginalPage(2);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it ('should give the value to the currentpage', () => {
|
||||
expect(comp.originalOptions.currentPage).toBe(2);
|
||||
})
|
||||
it ('should call the next function on the originalCurrentPage', (done) => {
|
||||
comp.originalCurrentPage$.subscribe((event) => {
|
||||
expect(event).toEqual(2);
|
||||
done();
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when we press the pageChange button for license bundle', () => {
|
||||
beforeEach(() => {
|
||||
comp.switchLicensePage(2);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it ('should give the value to the currentpage', () => {
|
||||
expect(comp.licenseOptions.currentPage).toBe(2);
|
||||
})
|
||||
it ('should call the next function on the licenseCurrentPage', (done) => {
|
||||
comp.licenseCurrentPage$.subscribe((event) => {
|
||||
expect(event).toEqual(2);
|
||||
done();
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@@ -1,13 +1,15 @@
|
||||
import { Component, Injector, Input, OnInit } from '@angular/core';
|
||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { map, startWith } from 'rxjs/operators';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||
|
||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators';
|
||||
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||
import { FileSectionComponent } from '../../../simple/field-components/file-section/file-section.component';
|
||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* This component renders the file section of the item
|
||||
@@ -25,7 +27,23 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
|
||||
|
||||
label: string;
|
||||
|
||||
bitstreams$: Observable<Bitstream[]>;
|
||||
originals$: Observable<RemoteData<PaginatedList<Bitstream>>>;
|
||||
licenses$: Observable<RemoteData<PaginatedList<Bitstream>>>;
|
||||
|
||||
pageSize = 5;
|
||||
originalOptions = Object.assign(new PaginationComponentOptions(),{
|
||||
id: 'original-bitstreams-options',
|
||||
currentPage: 1,
|
||||
pageSize: this.pageSize
|
||||
});
|
||||
originalCurrentPage$ = new BehaviorSubject<number>(1);
|
||||
|
||||
licenseOptions = Object.assign(new PaginationComponentOptions(),{
|
||||
id: 'license-bitstreams-options',
|
||||
currentPage: 1,
|
||||
pageSize: this.pageSize
|
||||
});
|
||||
licenseCurrentPage$ = new BehaviorSubject<number>(1);
|
||||
|
||||
constructor(
|
||||
bitstreamDataService: BitstreamDataService
|
||||
@@ -34,40 +52,45 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
// TODO pagination
|
||||
const originals$ = this.bitstreamDataService.findAllByItemAndBundleName(
|
||||
this.originals$ = this.originalCurrentPage$.pipe(
|
||||
switchMap((pageNumber: number) => this.bitstreamDataService.findAllByItemAndBundleName(
|
||||
this.item,
|
||||
'ORIGINAL',
|
||||
{ elementsPerPage: Number.MAX_SAFE_INTEGER },
|
||||
{ elementsPerPage: this.pageSize, currentPage: pageNumber },
|
||||
followLink( 'format')
|
||||
).pipe(
|
||||
getFirstSucceededRemoteListPayload(),
|
||||
startWith([])
|
||||
))
|
||||
);
|
||||
const licenses$ = this.bitstreamDataService.findAllByItemAndBundleName(
|
||||
|
||||
this.licenses$ = this.licenseCurrentPage$.pipe(
|
||||
switchMap((pageNumber: number) => this.bitstreamDataService.findAllByItemAndBundleName(
|
||||
this.item,
|
||||
'LICENSE',
|
||||
{ elementsPerPage: Number.MAX_SAFE_INTEGER },
|
||||
{ elementsPerPage: this.pageSize, currentPage: pageNumber },
|
||||
followLink( 'format')
|
||||
).pipe(
|
||||
getFirstSucceededRemoteListPayload(),
|
||||
startWith([])
|
||||
))
|
||||
);
|
||||
this.bitstreams$ = observableCombineLatest(originals$, licenses$).pipe(
|
||||
map(([o, l]) => [...o, ...l]),
|
||||
map((files: Bitstream[]) =>
|
||||
files.map(
|
||||
(original) => {
|
||||
original.thumbnail = this.bitstreamDataService.getMatchingThumbnail(this.item, original);
|
||||
return original;
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current page for the original bundle bitstreams
|
||||
* @param page
|
||||
*/
|
||||
switchOriginalPage(page: number) {
|
||||
this.originalOptions.currentPage = page;
|
||||
this.originalCurrentPage$.next(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current page for the license bundle bitstreams
|
||||
* @param page
|
||||
*/
|
||||
switchLicensePage(page: number) {
|
||||
this.licenseOptions.currentPage = page;
|
||||
this.licenseCurrentPage$.next(page);
|
||||
}
|
||||
}
|
||||
|
@@ -6,6 +6,13 @@
|
||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
||||
</ds-file-download-link>
|
||||
<ds-loading *ngIf="isLoading" message="{{'loading.default' | translate}}" [showMessage]="false"></ds-loading>
|
||||
<div *ngIf="!isLastPage" class="mt-1" id="view-more">
|
||||
<a class="bitstream-view-more btn btn-outline-secondary btn-sm" [routerLink]="" (click)="getNextPage()">{{'item.page.bitstreams.view-more' | translate}}</a>
|
||||
</div>
|
||||
<div *ngIf="isLastPage && currentPage != 1" class="mt-1" id="collapse">
|
||||
<a class="bitstream-collapse btn btn-outline-secondary btn-sm" [routerLink]="" (click)="currentPage = undefined; getNextPage();">{{'item.page.bitstreams.collapse' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ds-metadata-field-wrapper>
|
||||
</ng-container>
|
||||
|
@@ -0,0 +1,169 @@
|
||||
import {FileSectionComponent} from './file-section.component';
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';
|
||||
import {TranslateLoaderMock} from '../../../../shared/mocks/translate-loader.mock';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {VarDirective} from '../../../../shared/utils/var.directive';
|
||||
import {NO_ERRORS_SCHEMA} from '@angular/core';
|
||||
import {BitstreamDataService} from '../../../../core/data/bitstream-data.service';
|
||||
import {createSuccessfulRemoteDataObject$} from '../../../../shared/remote-data.utils';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {Bitstream} from '../../../../core/shared/bitstream.model';
|
||||
import {of as observableOf} from 'rxjs';
|
||||
import {MockBitstreamFormat1} from '../../../../shared/mocks/item.mock';
|
||||
import {FileSizePipe} from '../../../../shared/utils/file-size-pipe';
|
||||
import {PageInfo} from '../../../../core/shared/page-info.model';
|
||||
import {MetadataFieldWrapperComponent} from '../../../field-components/metadata-field-wrapper/metadata-field-wrapper.component';
|
||||
import {createPaginatedList} from '../../../../shared/testing/utils.test';
|
||||
|
||||
describe('FileSectionComponent', () => {
|
||||
let comp: FileSectionComponent;
|
||||
let fixture: ComponentFixture<FileSectionComponent>;
|
||||
|
||||
const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
||||
findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([]))
|
||||
});
|
||||
|
||||
const mockBitstream: Bitstream = Object.assign(new Bitstream(),
|
||||
{
|
||||
sizeBytes: 10201,
|
||||
content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||
format: observableOf(MockBitstreamFormat1),
|
||||
bundleName: 'ORIGINAL',
|
||||
_links: {
|
||||
self: {
|
||||
href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713'
|
||||
},
|
||||
content: {
|
||||
href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content'
|
||||
}
|
||||
},
|
||||
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
||||
type: 'bitstream',
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: null,
|
||||
value: 'test_word.docx'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}), BrowserAnimationsModule],
|
||||
declarations: [FileSectionComponent, VarDirective, FileSizePipe, MetadataFieldWrapperComponent],
|
||||
providers: [
|
||||
{provide: BitstreamDataService, useValue: bitstreamDataService}
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(async(() => {
|
||||
fixture = TestBed.createComponent(FileSectionComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
describe('when the bitstreams are loading', () => {
|
||||
beforeEach(() => {
|
||||
comp.bitstreams$.next([mockBitstream]);
|
||||
comp.isLoading = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should display a loading component', () => {
|
||||
const loading = fixture.debugElement.query(By.css('ds-loading'));
|
||||
expect(loading.nativeElement).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the "Show more" button is clicked', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
comp.bitstreams$.next([mockBitstream]);
|
||||
comp.currentPage = 1;
|
||||
comp.isLastPage = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should call the service to retrieve more bitstreams', () => {
|
||||
const viewMore = fixture.debugElement.query(By.css('.bitstream-view-more'));
|
||||
viewMore.triggerEventHandler('click', null);
|
||||
expect(bitstreamDataService.findAllByItemAndBundleName).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('one bitstream should be on the page', () => {
|
||||
const viewMore = fixture.debugElement.query(By.css('.bitstream-view-more'));
|
||||
viewMore.triggerEventHandler('click', null);
|
||||
const fileDownloadLink = fixture.debugElement.queryAll(By.css('ds-file-download-link'));
|
||||
expect(fileDownloadLink.length).toEqual(1);
|
||||
})
|
||||
|
||||
describe('when it is then clicked again', () => {
|
||||
beforeEach(() => {
|
||||
bitstreamDataService.findAllByItemAndBundleName.and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([mockBitstream])));
|
||||
const viewMore = fixture.debugElement.query(By.css('.bitstream-view-more'));
|
||||
viewMore.triggerEventHandler('click', null);
|
||||
fixture.detectChanges();
|
||||
|
||||
})
|
||||
it('should contain another bitstream', () => {
|
||||
const fileDownloadLink = fixture.debugElement.queryAll(By.css('ds-file-download-link'));
|
||||
expect(fileDownloadLink.length).toEqual(2);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
describe('when its the last page of bitstreams', () => {
|
||||
beforeEach(() => {
|
||||
comp.bitstreams$.next([mockBitstream]);
|
||||
comp.isLastPage = true;
|
||||
comp.currentPage = 2;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not contain a view more link', () => {
|
||||
const viewMore = fixture.debugElement.query(By.css('.bitstream-view-more'));
|
||||
expect(viewMore).toBeNull();
|
||||
})
|
||||
|
||||
it('should contain a view less link', () => {
|
||||
const viewLess = fixture.debugElement.query(By.css('.bitstream-collapse'));
|
||||
expect(viewLess).toBeDefined();
|
||||
})
|
||||
|
||||
it('clicking on the view less link should reset the pages and call getNextPage()', () => {
|
||||
const pageInfo = Object.assign(new PageInfo(), {
|
||||
elementsPerPage: 3,
|
||||
totalElements: 5,
|
||||
totalPages: 2,
|
||||
currentPage: 1,
|
||||
_links: {
|
||||
self: {href: 'https://rest.api/core/bitstreams/'},
|
||||
next: {href: 'https://rest.api/core/bitstreams?page=2'}
|
||||
}
|
||||
});
|
||||
const PaginatedList = Object.assign(createPaginatedList([mockBitstream]), {
|
||||
pageInfo: pageInfo
|
||||
});
|
||||
bitstreamDataService.findAllByItemAndBundleName.and.returnValue(createSuccessfulRemoteDataObject$(PaginatedList));
|
||||
const viewLess = fixture.debugElement.query(By.css('.bitstream-collapse'));
|
||||
viewLess.triggerEventHandler('click', null);
|
||||
expect(bitstreamDataService.findAllByItemAndBundleName).toHaveBeenCalled();
|
||||
expect(comp.currentPage).toBe(1);
|
||||
expect(comp.isLastPage).toBeFalse();
|
||||
})
|
||||
|
||||
})
|
||||
})
|
@@ -1,10 +1,13 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||
|
||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators';
|
||||
import { filter, takeWhile } from 'rxjs/operators';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { hasNoValue, hasValue } from '../../../../shared/empty.util';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||
|
||||
/**
|
||||
* This component renders the file section of the item
|
||||
@@ -22,7 +25,15 @@ export class FileSectionComponent implements OnInit {
|
||||
|
||||
separator = '<br/>';
|
||||
|
||||
bitstreams$: Observable<Bitstream[]>;
|
||||
bitstreams$: BehaviorSubject<Bitstream[]>;
|
||||
|
||||
currentPage: number;
|
||||
|
||||
isLoading: boolean;
|
||||
|
||||
isLastPage: boolean;
|
||||
|
||||
pageSize = 5;
|
||||
|
||||
constructor(
|
||||
protected bitstreamDataService: BitstreamDataService
|
||||
@@ -30,13 +41,31 @@ export class FileSectionComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initialize();
|
||||
this.getNextPage();
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
this.bitstreams$ = this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'ORIGINAL').pipe(
|
||||
getFirstSucceededRemoteListPayload()
|
||||
);
|
||||
/**
|
||||
* This method will retrieve the next page of Bitstreams from the external BitstreamDataService call.
|
||||
* It'll retrieve the currentPage from the class variables and it'll add the next page of bitstreams with the
|
||||
* already existing one.
|
||||
* If the currentPage variable is undefined, we'll set it to 1 and retrieve the first page of Bitstreams
|
||||
*/
|
||||
getNextPage(): void {
|
||||
this.isLoading = true;
|
||||
if (this.currentPage === undefined) {
|
||||
this.currentPage = 1;
|
||||
this.bitstreams$ = new BehaviorSubject([]);
|
||||
} else {
|
||||
this.currentPage++;
|
||||
}
|
||||
this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'ORIGINAL', { currentPage: this.currentPage, elementsPerPage: this.pageSize }).pipe(
|
||||
filter((bitstreamsRD: RemoteData<PaginatedList<Bitstream>>) => hasValue(bitstreamsRD)),
|
||||
takeWhile((bitstreamsRD: RemoteData<PaginatedList<Bitstream>>) => hasNoValue(bitstreamsRD.payload) && hasNoValue(bitstreamsRD.error), true)
|
||||
).subscribe((bitstreamsRD: RemoteData<PaginatedList<Bitstream>>) => {
|
||||
const current: Bitstream[] = this.bitstreams$.getValue();
|
||||
this.bitstreams$.next([...current, ...bitstreamsRD.payload.page]);
|
||||
this.isLoading = false;
|
||||
this.isLastPage = this.currentPage === bitstreamsRD.payload.totalPages;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ import { getItemPageRoute } from './+item-page/item-page-routing.module';
|
||||
import { getCollectionPageRoute } from './+collection-page/collection-page-routing.module';
|
||||
import { SiteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
|
||||
import { ReloadGuard } from './core/reload/reload.guard';
|
||||
import { EndUserAgreementGuard } from './core/end-user-agreement/end-user-agreement.guard';
|
||||
|
||||
const ITEM_MODULE_PATH = 'items';
|
||||
@@ -95,7 +96,7 @@ export function getInfoModulePath() {
|
||||
imports: [
|
||||
RouterModule.forRoot([
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{ path: 'reload/:rnd', redirectTo: '/home', pathMatch: 'full' },
|
||||
{ path: 'reload/:rnd', component: PageNotFoundComponent, pathMatch: 'full', canActivate: [ReloadGuard] },
|
||||
{ path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule', data: { showBreadcrumbs: false } },
|
||||
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule' },
|
||||
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' },
|
||||
|
@@ -1,4 +1,7 @@
|
||||
<div class="outer-wrapper">
|
||||
<div class="text-center ds-full-screen-loader d-flex align-items-center flex-column justify-content-center" *ngIf="!(hasAuthFinishedLoading$ | async)">
|
||||
<ds-loading [showMessage]="false"></ds-loading>
|
||||
</div>
|
||||
<div class="outer-wrapper" *ngIf="hasAuthFinishedLoading$ | async">
|
||||
<ds-admin-sidebar></ds-admin-sidebar>
|
||||
<div class="inner-wrapper" [@slideSidebarPadding]="{
|
||||
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
|
||||
|
@@ -47,3 +47,7 @@ ds-admin-sidebar {
|
||||
position: fixed;
|
||||
z-index: $sidebar-z-index;
|
||||
}
|
||||
|
||||
.ds-full-screen-loader {
|
||||
height: 100vh;
|
||||
}
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import * as ngrx from '@ngrx/store';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||
@@ -30,13 +29,13 @@ import { RouteService } from './core/services/route.service';
|
||||
import { MockActivatedRoute } from './shared/mocks/active-router.mock';
|
||||
import { RouterMock } from './shared/mocks/router.mock';
|
||||
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
||||
import { storeModuleConfig } from './app.reducer';
|
||||
import { AppState, storeModuleConfig } from './app.reducer';
|
||||
import { LocaleService } from './core/locale/locale.service';
|
||||
import { authReducer } from './core/auth/auth.reducer';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
|
||||
let comp: AppComponent;
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
let de: DebugElement;
|
||||
let el: HTMLElement;
|
||||
const menuService = new MenuServiceStub();
|
||||
|
||||
describe('App component', () => {
|
||||
@@ -52,7 +51,7 @@ describe('App component', () => {
|
||||
return TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
StoreModule.forRoot({}, storeModuleConfig),
|
||||
StoreModule.forRoot(authReducer, storeModuleConfig),
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
@@ -82,12 +81,19 @@ describe('App component', () => {
|
||||
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AppComponent);
|
||||
spyOnProperty(ngrx, 'select').and.callFake(() => {
|
||||
return () => {
|
||||
return () => cold('a', {
|
||||
a: {
|
||||
core: { auth: { loading: false } }
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(AppComponent);
|
||||
comp = fixture.componentInstance; // component test instance
|
||||
// query for the <div class='outer-wrapper'> by CSS element selector
|
||||
de = fixture.debugElement.query(By.css('div.outer-wrapper'));
|
||||
el = de.nativeElement;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create component', inject([AppComponent], (app: AppComponent) => {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { delay, filter, map, take } from 'rxjs/operators';
|
||||
import { delay, filter, map, take, distinctUntilChanged } from 'rxjs/operators';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
@@ -19,9 +19,8 @@ import { MetadataService } from './core/metadata/metadata.service';
|
||||
import { HostWindowResizeAction } from './shared/host-window.actions';
|
||||
import { HostWindowState } from './shared/search/host-window.reducer';
|
||||
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
|
||||
import { isAuthenticated } from './core/auth/selectors';
|
||||
import { isAuthenticated, isAuthenticationLoading } from './core/auth/selectors';
|
||||
import { AuthService } from './core/auth/auth.service';
|
||||
import variables from '../styles/_exposed_variables.scss';
|
||||
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
||||
import { MenuService } from './shared/menu/menu.service';
|
||||
import { MenuID } from './shared/menu/initial-menus-state';
|
||||
@@ -53,6 +52,11 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
notificationOptions = environment.notifications;
|
||||
models;
|
||||
|
||||
/**
|
||||
* Whether or not the authenticated has finished loading
|
||||
*/
|
||||
hasAuthFinishedLoading$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||
private translate: TranslateService,
|
||||
@@ -90,6 +94,10 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.hasAuthFinishedLoading$ = this.store.pipe(select(isAuthenticationLoading)).pipe(
|
||||
map((isLoading: boolean) => isLoading === false),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
const env: string = environment.production ? 'Production' : 'Development';
|
||||
const color: string = environment.production ? 'red' : 'green';
|
||||
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
||||
|
@@ -34,6 +34,7 @@ export const AuthActionTypes = {
|
||||
RETRIEVE_AUTHENTICATED_EPERSON: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON'),
|
||||
RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS'),
|
||||
RETRIEVE_AUTHENTICATED_EPERSON_ERROR: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_ERROR'),
|
||||
REDIRECT_AFTER_LOGIN_SUCCESS: type('dspace/auth/REDIRECT_AFTER_LOGIN_SUCCESS')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
@@ -335,6 +336,20 @@ export class SetRedirectUrlAction implements Action {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start loading for a hard redirect
|
||||
* @class StartHardRedirectLoadingAction
|
||||
* @implements {Action}
|
||||
*/
|
||||
export class RedirectAfterLoginSuccessAction implements Action {
|
||||
public type: string = AuthActionTypes.REDIRECT_AFTER_LOGIN_SUCCESS;
|
||||
payload: string;
|
||||
|
||||
constructor(url: string) {
|
||||
this.payload = url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the authenticated eperson.
|
||||
* @class RetrieveAuthenticatedEpersonAction
|
||||
@@ -402,8 +417,8 @@ export type AuthActions
|
||||
| RetrieveAuthMethodsSuccessAction
|
||||
| RetrieveAuthMethodsErrorAction
|
||||
| RetrieveTokenAction
|
||||
| ResetAuthenticationMessagesAction
|
||||
| RetrieveAuthenticatedEpersonAction
|
||||
| RetrieveAuthenticatedEpersonErrorAction
|
||||
| RetrieveAuthenticatedEpersonSuccessAction
|
||||
| SetRedirectUrlAction;
|
||||
| SetRedirectUrlAction
|
||||
| RedirectAfterLoginSuccessAction;
|
||||
|
@@ -27,6 +27,7 @@ import {
|
||||
CheckAuthenticationTokenCookieAction,
|
||||
LogOutErrorAction,
|
||||
LogOutSuccessAction,
|
||||
RedirectAfterLoginSuccessAction,
|
||||
RefreshTokenAction,
|
||||
RefreshTokenErrorAction,
|
||||
RefreshTokenSuccessAction,
|
||||
@@ -79,7 +80,26 @@ export class AuthEffects {
|
||||
public authenticatedSuccess$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.AUTHENTICATED_SUCCESS),
|
||||
tap((action: AuthenticatedSuccessAction) => this.authService.storeToken(action.payload.authToken)),
|
||||
map((action: AuthenticatedSuccessAction) => new RetrieveAuthenticatedEpersonAction(action.payload.userHref))
|
||||
switchMap((action: AuthenticatedSuccessAction) => this.authService.getRedirectUrl().pipe(
|
||||
take(1),
|
||||
map((redirectUrl: string) => [action, redirectUrl])
|
||||
)),
|
||||
map(([action, redirectUrl]: [AuthenticatedSuccessAction, string]) => {
|
||||
if (hasValue(redirectUrl)) {
|
||||
return new RedirectAfterLoginSuccessAction(redirectUrl);
|
||||
} else {
|
||||
return new RetrieveAuthenticatedEpersonAction(action.payload.userHref);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@Effect({ dispatch: false })
|
||||
public redirectAfterLoginSuccess$: Observable<Action> = this.actions$.pipe(
|
||||
ofType(AuthActionTypes.REDIRECT_AFTER_LOGIN_SUCCESS),
|
||||
tap((action: RedirectAfterLoginSuccessAction) => {
|
||||
this.authService.clearRedirectUrl();
|
||||
this.authService.navigateToRedirectUrl(action.payload);
|
||||
})
|
||||
);
|
||||
|
||||
// It means "reacts to this action but don't send another"
|
||||
@@ -201,13 +221,6 @@ export class AuthEffects {
|
||||
tap(() => this.authService.refreshAfterLogout())
|
||||
);
|
||||
|
||||
@Effect({ dispatch: false })
|
||||
public redirectToLogin$: Observable<Action> = this.actions$
|
||||
.pipe(ofType(AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED),
|
||||
tap(() => this.authService.removeToken()),
|
||||
tap(() => this.authService.redirectToLogin())
|
||||
);
|
||||
|
||||
@Effect({ dispatch: false })
|
||||
public redirectToLoginTokenExpired$: Observable<Action> = this.actions$
|
||||
.pipe(
|
||||
|
@@ -62,7 +62,7 @@ export interface AuthState {
|
||||
const initialState: AuthState = {
|
||||
authenticated: false,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
loading: undefined,
|
||||
authMethods: []
|
||||
};
|
||||
|
||||
@@ -201,6 +201,11 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
||||
redirectUrl: (action as SetRedirectUrlAction).payload,
|
||||
});
|
||||
|
||||
case AuthActionTypes.REDIRECT_AFTER_LOGIN_SUCCESS:
|
||||
return Object.assign({}, state, {
|
||||
loading: true,
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@@ -27,6 +27,7 @@ import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { authMethodsMock } from '../../shared/testing/auth-service.stub';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||
|
||||
describe('AuthService test', () => {
|
||||
|
||||
@@ -48,6 +49,7 @@ describe('AuthService test', () => {
|
||||
let authenticatedState;
|
||||
let unAuthenticatedState;
|
||||
let linkService;
|
||||
let hardRedirectService;
|
||||
|
||||
function init() {
|
||||
mockStore = jasmine.createSpyObj('store', {
|
||||
@@ -77,6 +79,7 @@ describe('AuthService test', () => {
|
||||
linkService = {
|
||||
resolveLinks: {}
|
||||
};
|
||||
hardRedirectService = jasmine.createSpyObj('hardRedirectService', ['redirect']);
|
||||
spyOn(linkService, 'resolveLinks').and.returnValue({ authenticated: true, eperson: observableOf({ payload: {} }) });
|
||||
|
||||
}
|
||||
@@ -104,6 +107,7 @@ describe('AuthService test', () => {
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
{ provide: Store, useValue: mockStore },
|
||||
{ provide: EPersonDataService, useValue: mockEpersonDataService },
|
||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||
CookieService,
|
||||
AuthService
|
||||
],
|
||||
@@ -210,7 +214,7 @@ describe('AuthService test', () => {
|
||||
(state as any).core = Object.create({});
|
||||
(state as any).core.auth = authenticatedState;
|
||||
});
|
||||
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store);
|
||||
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService);
|
||||
}));
|
||||
|
||||
it('should return true when user is logged in', () => {
|
||||
@@ -289,7 +293,7 @@ describe('AuthService test', () => {
|
||||
(state as any).core = Object.create({});
|
||||
(state as any).core.auth = authenticatedState;
|
||||
});
|
||||
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store);
|
||||
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService);
|
||||
storage = (authService as any).storage;
|
||||
routeServiceMock = TestBed.get(RouteService);
|
||||
routerStub = TestBed.get(Router);
|
||||
@@ -319,35 +323,30 @@ describe('AuthService test', () => {
|
||||
});
|
||||
|
||||
it('should set redirect url to previous page', () => {
|
||||
spyOn(routeServiceMock, 'getHistory').and.callThrough();
|
||||
spyOn(routerStub, 'navigateByUrl');
|
||||
authService.redirectAfterLoginSuccess(true);
|
||||
expect(routeServiceMock.getHistory).toHaveBeenCalled();
|
||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/collection/123');
|
||||
(storage.get as jasmine.Spy).and.returnValue('/collection/123');
|
||||
authService.redirectAfterLoginSuccess();
|
||||
// Reload with redirect URL set to /collection/123
|
||||
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*\\?redirect=' + encodeURIComponent('/collection/123'))));
|
||||
});
|
||||
|
||||
it('should set redirect url to current page', () => {
|
||||
spyOn(routeServiceMock, 'getHistory').and.callThrough();
|
||||
spyOn(routerStub, 'navigateByUrl');
|
||||
authService.redirectAfterLoginSuccess(false);
|
||||
expect(routeServiceMock.getHistory).toHaveBeenCalled();
|
||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/home');
|
||||
(storage.get as jasmine.Spy).and.returnValue('/home');
|
||||
authService.redirectAfterLoginSuccess();
|
||||
// Reload with redirect URL set to /home
|
||||
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*\\?redirect=' + encodeURIComponent('/home'))));
|
||||
});
|
||||
|
||||
it('should redirect to / and not to /login', () => {
|
||||
spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf(['/login', '/login']));
|
||||
spyOn(routerStub, 'navigateByUrl');
|
||||
authService.redirectAfterLoginSuccess(true);
|
||||
expect(routeServiceMock.getHistory).toHaveBeenCalled();
|
||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/');
|
||||
it('should redirect to regular reload and not to /login', () => {
|
||||
(storage.get as jasmine.Spy).and.returnValue('/login');
|
||||
authService.redirectAfterLoginSuccess();
|
||||
// Reload without a redirect URL
|
||||
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*(?!\\?)$')));
|
||||
});
|
||||
|
||||
it('should redirect to / when no redirect url is found', () => {
|
||||
spyOn(routeServiceMock, 'getHistory').and.returnValue(observableOf(['']));
|
||||
spyOn(routerStub, 'navigateByUrl');
|
||||
authService.redirectAfterLoginSuccess(true);
|
||||
expect(routeServiceMock.getHistory).toHaveBeenCalled();
|
||||
expect(routerStub.navigateByUrl).toHaveBeenCalledWith('/');
|
||||
it('should not redirect when no redirect url is found', () => {
|
||||
authService.redirectAfterLoginSuccess();
|
||||
// Reload without a redirect URL
|
||||
expect(hardRedirectService.redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('impersonate', () => {
|
||||
@@ -464,6 +463,14 @@ describe('AuthService test', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshAfterLogout', () => {
|
||||
it('should call navigateToRedirectUrl with no url', () => {
|
||||
spyOn(authService as any, 'navigateToRedirectUrl').and.stub();
|
||||
authService.refreshAfterLogout();
|
||||
expect((authService as any).navigateToRedirectUrl).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user is not logged in', () => {
|
||||
@@ -496,7 +503,7 @@ describe('AuthService test', () => {
|
||||
(state as any).core = Object.create({});
|
||||
(state as any).core.auth = unAuthenticatedState;
|
||||
});
|
||||
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store);
|
||||
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService);
|
||||
}));
|
||||
|
||||
it('should return null for the shortlived token', () => {
|
||||
|
@@ -14,7 +14,15 @@ import { AuthRequestService } from './auth-request.service';
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { AuthStatus } from './models/auth-status.model';
|
||||
import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model';
|
||||
import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util';
|
||||
import {
|
||||
hasValue,
|
||||
hasValueOperator,
|
||||
isEmpty,
|
||||
isNotEmpty,
|
||||
isNotNull,
|
||||
isNotUndefined,
|
||||
hasNoValue
|
||||
} from '../../shared/empty.util';
|
||||
import { CookieService } from '../services/cookie.service';
|
||||
import {
|
||||
getAuthenticatedUserId,
|
||||
@@ -36,6 +44,7 @@ import { RouteService } from '../services/route.service';
|
||||
import { EPersonDataService } from '../eperson/eperson-data.service';
|
||||
import { getAllSucceededRemoteDataPayload } from '../shared/operators';
|
||||
import { AuthMethod } from './models/auth.method';
|
||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||
|
||||
export const LOGIN_ROUTE = '/login';
|
||||
export const LOGOUT_ROUTE = '/logout';
|
||||
@@ -62,7 +71,8 @@ export class AuthService {
|
||||
protected router: Router,
|
||||
protected routeService: RouteService,
|
||||
protected storage: CookieService,
|
||||
protected store: Store<AppState>
|
||||
protected store: Store<AppState>,
|
||||
protected hardRedirectService: HardRedirectService
|
||||
) {
|
||||
this.store.pipe(
|
||||
select(isAuthenticated),
|
||||
@@ -409,69 +419,38 @@ export class AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the route navigated before the login
|
||||
* Perform a hard redirect to the URL
|
||||
* @param redirectUrl
|
||||
*/
|
||||
public redirectAfterLoginSuccess(isStandalonePage: boolean) {
|
||||
this.getRedirectUrl().pipe(
|
||||
take(1))
|
||||
.subscribe((redirectUrl) => {
|
||||
|
||||
if (isNotEmpty(redirectUrl)) {
|
||||
this.clearRedirectUrl();
|
||||
this.router.onSameUrlNavigation = 'reload';
|
||||
this.navigateToRedirectUrl(redirectUrl);
|
||||
} else {
|
||||
// If redirectUrl is empty use history.
|
||||
this.routeService.getHistory().pipe(
|
||||
take(1)
|
||||
).subscribe((history) => {
|
||||
let redirUrl;
|
||||
if (isStandalonePage) {
|
||||
// For standalone login pages, use the previous route.
|
||||
redirUrl = history[history.length - 2] || '';
|
||||
} else {
|
||||
redirUrl = history[history.length - 1] || '';
|
||||
}
|
||||
this.navigateToRedirectUrl(redirUrl);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
protected navigateToRedirectUrl(redirectUrl: string) {
|
||||
const url = decodeURIComponent(redirectUrl);
|
||||
// in case the user navigates directly to /login (via bookmark, etc), or the route history is not found.
|
||||
if (isEmpty(url) || url.startsWith(LOGIN_ROUTE)) {
|
||||
this.router.navigateByUrl('/');
|
||||
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */
|
||||
// this._window.nativeWindow.location.href = '/';
|
||||
} else {
|
||||
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */
|
||||
// this._window.nativeWindow.location.href = url;
|
||||
this.router.navigateByUrl(url);
|
||||
public navigateToRedirectUrl(redirectUrl: string) {
|
||||
let url = `/reload/${new Date().getTime()}`;
|
||||
if (isNotEmpty(redirectUrl) && !redirectUrl.startsWith(LOGIN_ROUTE)) {
|
||||
url += `?redirect=${encodeURIComponent(redirectUrl)}`;
|
||||
}
|
||||
this.hardRedirectService.redirect(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh route navigated
|
||||
*/
|
||||
public refreshAfterLogout() {
|
||||
// Hard redirect to the reload page with a unique number behind it
|
||||
// so that all state is definitely lost
|
||||
this._window.nativeWindow.location.href = `/reload/${new Date().getTime()}`;
|
||||
this.navigateToRedirectUrl(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get redirect url
|
||||
*/
|
||||
getRedirectUrl(): Observable<string> {
|
||||
const redirectUrl = this.storage.get(REDIRECT_COOKIE);
|
||||
if (isNotEmpty(redirectUrl)) {
|
||||
return observableOf(redirectUrl);
|
||||
return this.store.pipe(
|
||||
select(getRedirectUrl),
|
||||
map((urlFromStore: string) => {
|
||||
if (hasValue(urlFromStore)) {
|
||||
return urlFromStore;
|
||||
} else {
|
||||
return this.store.pipe(select(getRedirectUrl));
|
||||
return this.storage.get(REDIRECT_COOKIE);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -488,6 +467,16 @@ export class AuthService {
|
||||
this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : ''));
|
||||
}
|
||||
|
||||
setRedirectUrlIfNotSet(newRedirectUrl: string) {
|
||||
this.getRedirectUrl().pipe(
|
||||
take(1))
|
||||
.subscribe((currentRedirectUrl) => {
|
||||
if (hasNoValue(currentRedirectUrl)) {
|
||||
this.setRedirectUrl(newRedirectUrl);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear redirect url
|
||||
*/
|
||||
|
@@ -1,21 +1,27 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, CanLoad, Route, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
CanActivate,
|
||||
Route,
|
||||
Router,
|
||||
RouterStateSnapshot,
|
||||
UrlTree
|
||||
} from '@angular/router';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { map, find, switchMap } from 'rxjs/operators';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { isAuthenticated } from './selectors';
|
||||
import { AuthService } from './auth.service';
|
||||
import { RedirectWhenAuthenticationIsRequiredAction } from './auth.actions';
|
||||
import { isAuthenticated, isAuthenticationLoading } from './selectors';
|
||||
import { AuthService, LOGIN_ROUTE } from './auth.service';
|
||||
|
||||
/**
|
||||
* Prevent unauthorized activating and loading of routes
|
||||
* @class AuthenticatedGuard
|
||||
*/
|
||||
@Injectable()
|
||||
export class AuthenticatedGuard implements CanActivate, CanLoad {
|
||||
export class AuthenticatedGuard implements CanActivate {
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
@@ -24,46 +30,37 @@ export class AuthenticatedGuard implements CanActivate, CanLoad {
|
||||
|
||||
/**
|
||||
* True when user is authenticated
|
||||
* UrlTree with redirect to login page when user isn't authenticated
|
||||
* @method canActivate
|
||||
*/
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
||||
const url = state.url;
|
||||
return this.handleAuth(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* True when user is authenticated
|
||||
* UrlTree with redirect to login page when user isn't authenticated
|
||||
* @method canActivateChild
|
||||
*/
|
||||
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
|
||||
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
||||
return this.canActivate(route, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* True when user is authenticated
|
||||
* @method canLoad
|
||||
*/
|
||||
canLoad(route: Route): Observable<boolean> {
|
||||
const url = `/${route.path}`;
|
||||
|
||||
return this.handleAuth(url);
|
||||
}
|
||||
|
||||
private handleAuth(url: string): Observable<boolean> {
|
||||
// get observable
|
||||
const observable = this.store.pipe(select(isAuthenticated));
|
||||
|
||||
private handleAuth(url: string): Observable<boolean | UrlTree> {
|
||||
// redirect to sign in page if user is not authenticated
|
||||
observable.pipe(
|
||||
// .filter(() => isEmpty(this.router.routerState.snapshot.url) || this.router.routerState.snapshot.url === url)
|
||||
take(1))
|
||||
.subscribe((authenticated) => {
|
||||
if (!authenticated) {
|
||||
return this.store.pipe(select(isAuthenticationLoading)).pipe(
|
||||
find((isLoading: boolean) => isLoading === false),
|
||||
switchMap(() => this.store.pipe(select(isAuthenticated))),
|
||||
map((authenticated) => {
|
||||
if (authenticated) {
|
||||
return authenticated;
|
||||
} else {
|
||||
this.authService.setRedirectUrl(url);
|
||||
this.store.dispatch(new RedirectWhenAuthenticationIsRequiredAction('Login required'));
|
||||
this.authService.removeToken();
|
||||
return this.router.createUrlTree([LOGIN_ROUTE]);
|
||||
}
|
||||
});
|
||||
|
||||
return observable;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -58,32 +58,4 @@ export class ServerAuthService extends AuthService {
|
||||
map((status: AuthStatus) => Object.assign(new AuthStatus(), status))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the route navigated before the login
|
||||
*/
|
||||
public redirectAfterLoginSuccess(isStandalonePage: boolean) {
|
||||
this.getRedirectUrl().pipe(
|
||||
take(1))
|
||||
.subscribe((redirectUrl) => {
|
||||
if (isNotEmpty(redirectUrl)) {
|
||||
// override the route reuse strategy
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => {
|
||||
return false;
|
||||
};
|
||||
this.router.navigated = false;
|
||||
const url = decodeURIComponent(redirectUrl);
|
||||
this.router.navigateByUrl(url);
|
||||
} else {
|
||||
// If redirectUrl is empty use history. For ssr the history array should contain the requested url.
|
||||
this.routeService.getHistory().pipe(
|
||||
filter((history) => history.length > 0),
|
||||
take(1)
|
||||
).subscribe((history) => {
|
||||
this.navigateToRedirectUrl(history[history.length - 1] || '');
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -139,8 +139,8 @@ export class RemoteDataBuildService {
|
||||
const pageInfo$ = requestEntry$.pipe(
|
||||
filterSuccessfulResponses(),
|
||||
map((response: DSOSuccessResponse) => {
|
||||
if (hasValue((response as DSOSuccessResponse).pageInfo)) {
|
||||
return (response as DSOSuccessResponse).pageInfo;
|
||||
if (hasValue(response.pageInfo)) {
|
||||
return Object.assign(new PageInfo(), response.pageInfo);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@@ -162,6 +162,7 @@ import { SubmissionCcLicenceUrl } from './submission/models/submission-cc-licens
|
||||
import { SubmissionCcLicenseUrlDataService } from './submission/submission-cc-license-url-data.service';
|
||||
import { ConfigurationDataService } from './data/configuration-data.service';
|
||||
import { ConfigurationProperty } from './shared/configuration-property.model';
|
||||
import { ReloadGuard } from './reload/reload.guard';
|
||||
import { EndUserAgreementGuard } from './end-user-agreement/end-user-agreement.guard';
|
||||
import { EndUserAgreementService } from './end-user-agreement/end-user-agreement.service';
|
||||
|
||||
@@ -291,6 +292,7 @@ const PROVIDERS = [
|
||||
MetadataSchemaDataService,
|
||||
MetadataFieldDataService,
|
||||
TokenResponseParsingService,
|
||||
ReloadGuard,
|
||||
EndUserAgreementGuard,
|
||||
EndUserAgreementService,
|
||||
// register AuthInterceptor as HttpInterceptor
|
||||
|
@@ -12,12 +12,17 @@ import { Script } from '../../../process-page/scripts/script.model';
|
||||
import { ProcessParameter } from '../../../process-page/processes/process-parameter.model';
|
||||
import { find, map, switchMap } from 'rxjs/operators';
|
||||
import { URLCombiner } from '../../url-combiner/url-combiner';
|
||||
import { RemoteData } from '../remote-data';
|
||||
import { MultipartPostRequest, RestRequest } from '../request.models';
|
||||
import { RequestService } from '../request.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { RequestEntry } from '../request.reducer';
|
||||
import { dataService } from '../../cache/builders/build-decorators';
|
||||
import { SCRIPT } from '../../../process-page/scripts/script.resource-type';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
|
||||
export const METADATA_IMPORT_SCRIPT_NAME = 'metadata-import';
|
||||
export const METADATA_EXPORT_SCRIPT_NAME = 'metadata-export';
|
||||
|
||||
@Injectable()
|
||||
@dataService(SCRIPT)
|
||||
@@ -58,4 +63,16 @@ export class ScriptDataService extends DataService<Script> {
|
||||
});
|
||||
return form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a script with given name exist; user needs to be allowed to execute script for this to to not throw a 401 Unauthorized
|
||||
* @param scriptName script we want to check exists (and we can execute)
|
||||
*/
|
||||
public scriptWithNameExistsAndCanExecute(scriptName: string): Observable<boolean> {
|
||||
return this.findById(scriptName).pipe(
|
||||
find((rd: RemoteData<Script>) => hasValue(rd.payload) || hasValue(rd.error)),
|
||||
map((rd: RemoteData<Script>) => {
|
||||
return hasValue(rd.payload);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
47
src/app/core/reload/reload.guard.spec.ts
Normal file
47
src/app/core/reload/reload.guard.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { ReloadGuard } from './reload.guard';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
describe('ReloadGuard', () => {
|
||||
let guard: ReloadGuard;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(() => {
|
||||
router = jasmine.createSpyObj('router', ['parseUrl', 'createUrlTree']);
|
||||
guard = new ReloadGuard(router);
|
||||
});
|
||||
|
||||
describe('canActivate', () => {
|
||||
let route;
|
||||
|
||||
describe('when the route\'s query params contain a redirect url', () => {
|
||||
let redirectUrl;
|
||||
|
||||
beforeEach(() => {
|
||||
redirectUrl = '/redirect/url?param=extra';
|
||||
route = {
|
||||
queryParams: {
|
||||
redirect: redirectUrl
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should create a UrlTree with the redirect URL', () => {
|
||||
guard.canActivate(route, undefined);
|
||||
expect(router.parseUrl).toHaveBeenCalledWith(redirectUrl);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the route\'s query params doesn\'t contain a redirect url', () => {
|
||||
beforeEach(() => {
|
||||
route = {
|
||||
queryParams: {}
|
||||
};
|
||||
});
|
||||
|
||||
it('should create a UrlTree to home', () => {
|
||||
guard.canActivate(route, undefined);
|
||||
expect(router.createUrlTree).toHaveBeenCalledWith(['home']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
26
src/app/core/reload/reload.guard.ts
Normal file
26
src/app/core/reload/reload.guard.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* A guard redirecting the user to the URL provided in the route's query params
|
||||
* When no redirect url is found, the user is redirected to the homepage
|
||||
*/
|
||||
@Injectable()
|
||||
export class ReloadGuard implements CanActivate {
|
||||
constructor(private router: Router) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the UrlTree of the URL to redirect to
|
||||
* @param route
|
||||
* @param state
|
||||
*/
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): UrlTree {
|
||||
if (isNotEmpty(route.queryParams.redirect)) {
|
||||
return this.router.parseUrl(route.queryParams.redirect);
|
||||
} else {
|
||||
return this.router.createUrlTree(['home']);
|
||||
}
|
||||
}
|
||||
}
|
41
src/app/core/services/browser-hard-redirect.service.spec.ts
Normal file
41
src/app/core/services/browser-hard-redirect.service.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {async, TestBed} from '@angular/core/testing';
|
||||
import {BrowserHardRedirectService} from './browser-hard-redirect.service';
|
||||
|
||||
describe('BrowserHardRedirectService', () => {
|
||||
|
||||
const mockLocation = {
|
||||
href: undefined,
|
||||
pathname: '/pathname',
|
||||
search: '/search',
|
||||
} as Location;
|
||||
|
||||
const service: BrowserHardRedirectService = new BrowserHardRedirectService(mockLocation);
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when performing a redirect', () => {
|
||||
|
||||
const redirect = 'test redirect';
|
||||
|
||||
beforeEach(() => {
|
||||
service.redirect(redirect);
|
||||
});
|
||||
|
||||
it('should update the location', () => {
|
||||
expect(mockLocation.href).toEqual(redirect);
|
||||
})
|
||||
});
|
||||
|
||||
describe('when requesting the current route', () => {
|
||||
|
||||
it('should return the location origin', () => {
|
||||
expect(service.getCurrentRoute()).toEqual(mockLocation.pathname + mockLocation.search);
|
||||
});
|
||||
});
|
||||
});
|
30
src/app/core/services/browser-hard-redirect.service.ts
Normal file
30
src/app/core/services/browser-hard-redirect.service.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { LocationToken } from '../../../modules/app/browser-app.module';
|
||||
import { HardRedirectService } from './hard-redirect.service';
|
||||
|
||||
/**
|
||||
* Service for performing hard redirects within the browser app module
|
||||
*/
|
||||
@Injectable()
|
||||
export class BrowserHardRedirectService implements HardRedirectService {
|
||||
|
||||
constructor(
|
||||
@Inject(LocationToken) protected location: Location,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a hard redirect to URL
|
||||
* @param url
|
||||
*/
|
||||
redirect(url: string) {
|
||||
this.location.href = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the origin of a request
|
||||
*/
|
||||
getCurrentRoute() {
|
||||
return this.location.pathname + this.location.search;
|
||||
}
|
||||
}
|
22
src/app/core/services/hard-redirect.service.ts
Normal file
22
src/app/core/services/hard-redirect.service.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Service to take care of hard redirects
|
||||
*/
|
||||
@Injectable()
|
||||
export abstract class HardRedirectService {
|
||||
|
||||
/**
|
||||
* Perform a hard redirect to a given location.
|
||||
*
|
||||
* @param url
|
||||
* the page to redirect to
|
||||
*/
|
||||
abstract redirect(url: string);
|
||||
|
||||
/**
|
||||
* Get the current route, with query params included
|
||||
* e.g. /search?page=1&query=open%20access&f.dateIssued.min=1980&f.dateIssued.max=2020
|
||||
*/
|
||||
abstract getCurrentRoute();
|
||||
}
|
43
src/app/core/services/server-hard-redirect.service.spec.ts
Normal file
43
src/app/core/services/server-hard-redirect.service.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { ServerHardRedirectService } from './server-hard-redirect.service';
|
||||
|
||||
describe('ServerHardRedirectService', () => {
|
||||
|
||||
const mockRequest = jasmine.createSpyObj(['get']);
|
||||
const mockResponse = jasmine.createSpyObj(['redirect', 'end']);
|
||||
|
||||
const service: ServerHardRedirectService = new ServerHardRedirectService(mockRequest, mockResponse);
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when performing a redirect', () => {
|
||||
|
||||
const redirect = 'test redirect';
|
||||
|
||||
beforeEach(() => {
|
||||
service.redirect(redirect);
|
||||
});
|
||||
|
||||
it('should update the response object', () => {
|
||||
expect(mockResponse.redirect).toHaveBeenCalledWith(302, redirect);
|
||||
expect(mockResponse.end).toHaveBeenCalled();
|
||||
})
|
||||
});
|
||||
|
||||
describe('when requesting the current route', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest.originalUrl = 'original/url';
|
||||
});
|
||||
|
||||
it('should return the location origin', () => {
|
||||
expect(service.getCurrentRoute()).toEqual(mockRequest.originalUrl);
|
||||
});
|
||||
});
|
||||
});
|
62
src/app/core/services/server-hard-redirect.service.ts
Normal file
62
src/app/core/services/server-hard-redirect.service.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { Request, Response } from 'express';
|
||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||
import { HardRedirectService } from './hard-redirect.service';
|
||||
|
||||
/**
|
||||
* Service for performing hard redirects within the server app module
|
||||
*/
|
||||
@Injectable()
|
||||
export class ServerHardRedirectService implements HardRedirectService {
|
||||
|
||||
constructor(
|
||||
@Inject(REQUEST) protected req: Request,
|
||||
@Inject(RESPONSE) protected res: Response,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a hard redirect to URL
|
||||
* @param url
|
||||
*/
|
||||
redirect(url: string) {
|
||||
|
||||
if (url === this.req.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.res.finished) {
|
||||
const req: any = this.req;
|
||||
req._r_count = (req._r_count || 0) + 1;
|
||||
|
||||
console.warn('Attempted to redirect on a finished response. From',
|
||||
this.req.url, 'to', url);
|
||||
|
||||
if (req._r_count > 10) {
|
||||
console.error('Detected a redirection loop. killing the nodejs process');
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
// attempt to use the already set status
|
||||
let status = this.res.statusCode || 0;
|
||||
if (status < 300 || status >= 400) {
|
||||
// temporary redirect
|
||||
status = 302;
|
||||
}
|
||||
|
||||
console.log(`Redirecting from ${this.req.url} to ${url} with ${status}`);
|
||||
this.res.redirect(status, url);
|
||||
this.res.end();
|
||||
// I haven't found a way to correctly stop Angular rendering.
|
||||
// So we just let it end its work, though we have already closed
|
||||
// the response.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the origin of a request
|
||||
*/
|
||||
getCurrentRoute() {
|
||||
return this.req.originalUrl;
|
||||
}
|
||||
}
|
@@ -4,4 +4,5 @@ export enum DSpaceObjectType {
|
||||
ITEM = 'ITEM',
|
||||
COLLECTION = 'COLLECTION',
|
||||
COMMUNITY = 'COMMUNITY',
|
||||
DSPACEOBJECT = 'DSPACEOBJECT',
|
||||
}
|
||||
|
@@ -61,7 +61,7 @@ describe('HALEndpointService', () => {
|
||||
describe('getRootEndpointMap', () => {
|
||||
it('should configure a new EndpointMapRequest', () => {
|
||||
(service as any).getRootEndpointMap();
|
||||
const expected = new EndpointMapRequest(requestService.generateRequestId(), environment.rest.baseUrl);
|
||||
const expected = new EndpointMapRequest(requestService.generateRequestId(), environment.rest.baseUrl + '/api');
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
|
@@ -16,7 +16,7 @@ export class HALEndpointService {
|
||||
}
|
||||
|
||||
protected getRootHref(): string {
|
||||
return new RESTURLCombiner('/').toString();
|
||||
return new RESTURLCombiner('/api').toString();
|
||||
}
|
||||
|
||||
protected getRootEndpointMap(): Observable<EndpointMap> {
|
||||
|
@@ -5,13 +5,13 @@
|
||||
id="browseDropdown" (click)="toggleSection($event)"
|
||||
data-toggle="dropdown">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(section.id); injector: itemInjectors.get(section.id);"></ng-container>
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</a>
|
||||
<ul @slide *ngIf="(active | async)" (click)="deactivateSection($event)"
|
||||
class="m-0 shadow-none border-top-0 dropdown-menu show">
|
||||
<ng-container *ngFor="let subSection of (subSections | async)">
|
||||
<ng-container *ngFor="let subSection of (subSections$ | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(subSection.id); injector: itemInjectors.get(subSection.id);"></ng-container>
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</li>
|
@@ -1,4 +1,4 @@
|
||||
<li class="nav-item">
|
||||
<ng-container
|
||||
*ngComponentOutlet="itemComponents.get(section.id); injector: itemInjectors.get(section.id);"></ng-container>
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</li>
|
@@ -7,7 +7,7 @@
|
||||
<ul class="navbar-nav mr-auto shadow-none">
|
||||
<ng-container *ngFor="let section of (sections | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="sectionComponents.get(section.id); injector: sectionInjectors.get(section.id);"></ng-container>
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</div>
|
||||
|
@@ -0,0 +1,16 @@
|
||||
<div>
|
||||
<div class="modal-header">{{ headerLabel | translate:{dsoName: dso?.name} }}
|
||||
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{ infoLabel | translate:{dsoName: dso?.name} }}</p>
|
||||
<button type="button" class="cancel btn btn-secondary" (click)="cancelPressed()" aria-label="Cancel">
|
||||
{{ cancelLabel | translate:{dsoName: dso?.name} }}
|
||||
</button>
|
||||
<button type="button" class="confirm btn btn-primary" (click)="confirmPressed()" aria-label="Confirm" ngbAutofocus>
|
||||
{{ confirmLabel | translate:{dsoName: dso?.name} }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,126 @@
|
||||
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ConfirmationModalComponent } from './confirmation-modal.component';
|
||||
|
||||
describe('ConfirmationModalComponent', () => {
|
||||
let component: ConfirmationModalComponent;
|
||||
let fixture: ComponentFixture<ConfirmationModalComponent>;
|
||||
let debugElement: DebugElement;
|
||||
|
||||
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ConfirmationModalComponent],
|
||||
providers: [
|
||||
{ provide: NgbActiveModal, useValue: modalStub }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ConfirmationModalComponent);
|
||||
component = fixture.componentInstance;
|
||||
debugElement = fixture.debugElement;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('close', () => {
|
||||
beforeEach(() => {
|
||||
component.close();
|
||||
});
|
||||
it('should call the close method on the active modal', () => {
|
||||
expect(modalStub.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('confirmPressed', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(component.response, 'next');
|
||||
component.confirmPressed();
|
||||
});
|
||||
it('should call the close method on the active modal', () => {
|
||||
expect(modalStub.close).toHaveBeenCalled();
|
||||
});
|
||||
it('behaviour subject should have true as next', () => {
|
||||
expect(component.response.next).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelPressed', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(component.response, 'next');
|
||||
component.cancelPressed();
|
||||
});
|
||||
it('should call the close method on the active modal', () => {
|
||||
expect(modalStub.close).toHaveBeenCalled();
|
||||
});
|
||||
it('behaviour subject should have false as next', () => {
|
||||
expect(component.response.next).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the click method emits on close button', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
spyOn(component, 'close');
|
||||
debugElement.query(By.css('button.close')).triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('should call the close method on the component', () => {
|
||||
expect(component.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the click method emits on cancel button', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
spyOn(component, 'close');
|
||||
spyOn(component.response, 'next');
|
||||
debugElement.query(By.css('button.cancel')).triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('should call the close method on the component', () => {
|
||||
expect(component.close).toHaveBeenCalled();
|
||||
});
|
||||
it('behaviour subject should have false as next', () => {
|
||||
expect(component.response.next).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the click method emits on confirm button', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
spyOn(component, 'close');
|
||||
spyOn(component.response, 'next');
|
||||
debugElement.query(By.css('button.confirm')).triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('should call the close method on the component', () => {
|
||||
expect(component.close).toHaveBeenCalled();
|
||||
});
|
||||
it('behaviour subject should have true as next', () => {
|
||||
expect(component.response.next).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,48 @@
|
||||
import { Component, Input, Output } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Subject } from 'rxjs/internal/Subject';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-confirmation-modal',
|
||||
templateUrl: 'confirmation-modal.component.html',
|
||||
})
|
||||
export class ConfirmationModalComponent {
|
||||
@Input() headerLabel: string;
|
||||
@Input() infoLabel: string;
|
||||
@Input() cancelLabel: string;
|
||||
@Input() confirmLabel: string;
|
||||
@Input() dso: DSpaceObject;
|
||||
|
||||
/**
|
||||
* An event fired when the cancel or confirm button is clicked, with respectively false or true
|
||||
*/
|
||||
@Output()
|
||||
response: Subject<boolean> = new Subject();
|
||||
|
||||
constructor(protected activeModal: NgbActiveModal) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the action that led to the modal
|
||||
*/
|
||||
confirmPressed() {
|
||||
this.response.next(true);
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the action that led to the modal and close modal
|
||||
*/
|
||||
cancelPressed() {
|
||||
this.response.next(false);
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal
|
||||
*/
|
||||
close() {
|
||||
this.activeModal.close();
|
||||
}
|
||||
}
|
@@ -15,6 +15,7 @@
|
||||
class="list-group-item list-group-item-action border-0 list-entry"
|
||||
title="{{ listEntry.indexableObject.name }}"
|
||||
(click)="onSelect.emit(listEntry.indexableObject)" #listEntryElement>
|
||||
<ds-listable-object-component-loader [object]="listEntry" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
||||
<ds-listable-object-component-loader [object]="listEntry" [viewMode]="viewMode"
|
||||
[linkType]=linkTypes.None></ds-listable-object-component-loader>
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -13,6 +13,7 @@ import { FormControl } from '@angular/forms';
|
||||
import { Observable } from 'rxjs';
|
||||
import { debounceTime, startWith, switchMap } from 'rxjs/operators';
|
||||
import { SearchService } from '../../../core/shared/search/search.service';
|
||||
import { CollectionElementLinkType } from '../../object-collection/collection-element-link.type';
|
||||
import { PaginatedSearchOptions } from '../../search/paginated-search-options.model';
|
||||
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
@@ -76,6 +77,11 @@ export class DSOSelectorComponent implements OnInit {
|
||||
*/
|
||||
debounceTime = 500;
|
||||
|
||||
/**
|
||||
* The available link types
|
||||
*/
|
||||
linkTypes = CollectionElementLinkType;
|
||||
|
||||
constructor(private searchService: SearchService) {
|
||||
}
|
||||
|
||||
@@ -93,7 +99,7 @@ export class DSOSelectorComponent implements OnInit {
|
||||
return this.searchService.search(
|
||||
new PaginatedSearchOptions({
|
||||
query: query,
|
||||
dsoType: this.type,
|
||||
dsoType: this.type !== DSpaceObjectType.DSPACEOBJECT ? this.type : null,
|
||||
pagination: this.defaultPagination
|
||||
})
|
||||
)
|
||||
|
@@ -1,16 +1,15 @@
|
||||
import { Injectable, Input, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
|
||||
import { hasValue, isNotEmpty } from '../../empty.util';
|
||||
|
||||
export enum SelectorActionType {
|
||||
CREATE = 'create',
|
||||
EDIT = 'edit'
|
||||
EDIT = 'edit',
|
||||
EXPORT_METADATA = 'export-metadata'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,7 +24,7 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit {
|
||||
@Input() dsoRD: RemoteData<DSpaceObject>;
|
||||
|
||||
/**
|
||||
* The type of the DSO that's being edited or created
|
||||
* The type of the DSO that's being edited, created or exported
|
||||
*/
|
||||
objectType: DSpaceObjectType;
|
||||
|
||||
|
@@ -1,12 +1,10 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { EditItemSelectorComponent } from './edit-item-selector.component';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { RouterStub } from '../../../testing/router.stub';
|
||||
import * as itemRouter from '../../../../+item-page/item-page-routing.module';
|
||||
import { MetadataValue } from '../../../../core/shared/metadata.models';
|
||||
|
@@ -0,0 +1,211 @@
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { DebugElement, NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NgbActiveModal, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { METADATA_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { Community } from '../../../../core/shared/community.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ProcessParameter } from '../../../../process-page/processes/process-parameter.model';
|
||||
import { ConfirmationModalComponent } from '../../../confirmation-modal/confirmation-modal.component';
|
||||
import { TranslateLoaderMock } from '../../../mocks/translate-loader.mock';
|
||||
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
||||
import { ExportMetadataSelectorComponent } from './export-metadata-selector.component';
|
||||
|
||||
// No way to add entryComponents yet to testbed; alternative implemented; source: https://stackoverflow.com/questions/41689468/how-to-shallow-test-a-component-with-an-entrycomponents
|
||||
@NgModule({
|
||||
imports: [ NgbModalModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
],
|
||||
exports: [],
|
||||
declarations: [ConfirmationModalComponent],
|
||||
providers: [],
|
||||
entryComponents: [ConfirmationModalComponent],
|
||||
})
|
||||
class ModelTestModule { }
|
||||
|
||||
describe('ExportMetadataSelectorComponent', () => {
|
||||
let component: ExportMetadataSelectorComponent;
|
||||
let fixture: ComponentFixture<ExportMetadataSelectorComponent>;
|
||||
let debugElement: DebugElement;
|
||||
let modalRef;
|
||||
|
||||
let router;
|
||||
let notificationService: NotificationsServiceStub;
|
||||
let scriptService;
|
||||
|
||||
const mockItem = Object.assign(new Item(), {
|
||||
id: 'fake-id',
|
||||
handle: 'fake/handle',
|
||||
lastModified: '2018'
|
||||
});
|
||||
|
||||
const mockCollection: Collection = Object.assign(new Collection(), {
|
||||
id: 'test-collection-1-1',
|
||||
name: 'test-collection-1',
|
||||
handle: 'fake/test-collection-1',
|
||||
});
|
||||
|
||||
const mockCommunity = Object.assign(new Community(), {
|
||||
id: 'test-uuid',
|
||||
handle: 'fake/test-community-1',
|
||||
});
|
||||
|
||||
const itemRD = createSuccessfulRemoteDataObject(mockItem);
|
||||
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
|
||||
|
||||
beforeEach(async(() => {
|
||||
notificationService = new NotificationsServiceStub();
|
||||
router = jasmine.createSpyObj('router', {
|
||||
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||
});
|
||||
scriptService = jasmine.createSpyObj('scriptService',
|
||||
{
|
||||
invoke: observableOf({
|
||||
response:
|
||||
{
|
||||
isSuccessful: true,
|
||||
resourceSelfLinks: ['https://localhost:8080/api/core/processes/45']
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), ModelTestModule],
|
||||
declarations: [ExportMetadataSelectorComponent],
|
||||
providers: [
|
||||
{ provide: NgbActiveModal, useValue: modalStub },
|
||||
{ provide: NotificationsService, useValue: notificationService },
|
||||
{ provide: ScriptDataService, useValue: scriptService },
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
root: {
|
||||
snapshot: {
|
||||
data: {
|
||||
dso: itemRD,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Router, useValue: router
|
||||
}
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ExportMetadataSelectorComponent);
|
||||
component = fixture.componentInstance;
|
||||
debugElement = fixture.debugElement;
|
||||
const modalService = TestBed.get(NgbModal);
|
||||
modalRef = modalService.open(ConfirmationModalComponent);
|
||||
modalRef.componentInstance.response = observableOf(true);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('if item is selected', () => {
|
||||
let scriptRequestSucceeded;
|
||||
beforeEach((done) => {
|
||||
component.navigate(mockItem).subscribe((succeeded: boolean) => {
|
||||
scriptRequestSucceeded = succeeded;
|
||||
done()
|
||||
});
|
||||
});
|
||||
it('should show error notification', () => {
|
||||
expect(notificationService.error).toHaveBeenCalled();
|
||||
expect(scriptRequestSucceeded).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if collection is selected', () => {
|
||||
let scriptRequestSucceeded;
|
||||
beforeEach((done) => {
|
||||
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||
component.navigate(mockCollection).subscribe((succeeded: boolean) => {
|
||||
scriptRequestSucceeded = succeeded;
|
||||
done()
|
||||
});
|
||||
});
|
||||
it('metadata-export script is invoked with its -i handle and -f uuid.csv', () => {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '-i', value: mockCollection.handle }),
|
||||
Object.assign(new ProcessParameter(), { name: '-f', value: mockCollection.uuid + '.csv' }),
|
||||
];
|
||||
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_EXPORT_SCRIPT_NAME, parameterValues, []);
|
||||
});
|
||||
it('success notification is shown', () => {
|
||||
expect(scriptRequestSucceeded).toBeTrue();
|
||||
expect(notificationService.success).toHaveBeenCalled();
|
||||
});
|
||||
it('redirected to process page', () => {
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
|
||||
});
|
||||
});
|
||||
|
||||
describe('if community is selected', () => {
|
||||
let scriptRequestSucceeded;
|
||||
beforeEach((done) => {
|
||||
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||
component.navigate(mockCommunity).subscribe((succeeded: boolean) => {
|
||||
scriptRequestSucceeded = succeeded;
|
||||
done()
|
||||
});
|
||||
});
|
||||
it('metadata-export script is invoked with its -i handle and -f uuid.csv', () => {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '-i', value: mockCommunity.handle }),
|
||||
Object.assign(new ProcessParameter(), { name: '-f', value: mockCommunity.uuid + '.csv' }),
|
||||
];
|
||||
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_EXPORT_SCRIPT_NAME, parameterValues, []);
|
||||
});
|
||||
it('success notification is shown', () => {
|
||||
expect(scriptRequestSucceeded).toBeTrue();
|
||||
expect(notificationService.success).toHaveBeenCalled();
|
||||
});
|
||||
it('redirected to process page', () => {
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
|
||||
});
|
||||
});
|
||||
|
||||
describe('if community/collection is selected; but script invoke fails', () => {
|
||||
let scriptRequestSucceeded;
|
||||
beforeEach((done) => {
|
||||
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||
jasmine.getEnv().allowRespy(true);
|
||||
spyOn(scriptService, 'invoke').and.returnValue(observableOf({
|
||||
response:
|
||||
{
|
||||
isSuccessful: false,
|
||||
}
|
||||
}));
|
||||
component.navigate(mockCommunity).subscribe((succeeded: boolean) => {
|
||||
scriptRequestSucceeded = succeeded;
|
||||
done()
|
||||
});
|
||||
});
|
||||
it('error notification is shown', () => {
|
||||
expect(scriptRequestSucceeded).toBeFalse();
|
||||
expect(notificationService.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,106 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { take, map, switchMap } from 'rxjs/operators';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { METADATA_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
|
||||
import { RequestEntry } from '../../../../core/data/request.reducer';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { Community } from '../../../../core/shared/community.model';
|
||||
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
|
||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ProcessParameter } from '../../../../process-page/processes/process-parameter.model';
|
||||
import { ConfirmationModalComponent } from '../../../confirmation-modal/confirmation-modal.component';
|
||||
import { isNotEmpty } from '../../../empty.util';
|
||||
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
||||
import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../dso-selector-modal-wrapper.component';
|
||||
|
||||
/**
|
||||
* Component to wrap a list of existing dso's inside a modal
|
||||
* Used to choose a dso from to export metadata of
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-export-metadata-selector',
|
||||
templateUrl: '../dso-selector-modal-wrapper.component.html',
|
||||
})
|
||||
export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
|
||||
objectType = DSpaceObjectType.DSPACEOBJECT;
|
||||
selectorType = DSpaceObjectType.DSPACEOBJECT;
|
||||
action = SelectorActionType.EXPORT_METADATA;
|
||||
|
||||
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router,
|
||||
protected notificationsService: NotificationsService, protected translationService: TranslateService,
|
||||
protected scriptDataService: ScriptDataService,
|
||||
private modalService: NgbModal) {
|
||||
super(activeModal, route);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the dso is a collection or community: start export-metadata script & navigate to process if successful
|
||||
* Otherwise show error message
|
||||
*/
|
||||
navigate(dso: DSpaceObject): Observable<boolean> {
|
||||
if (dso instanceof Collection || dso instanceof Community) {
|
||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||
modalRef.componentInstance.dso = dso;
|
||||
modalRef.componentInstance.headerLabel = 'confirmation-modal.export-metadata.header';
|
||||
modalRef.componentInstance.infoLabel = 'confirmation-modal.export-metadata.info';
|
||||
modalRef.componentInstance.cancelLabel = 'confirmation-modal.export-metadata.cancel';
|
||||
modalRef.componentInstance.confirmLabel = 'confirmation-modal.export-metadata.confirm';
|
||||
const resp$ = modalRef.componentInstance.response.pipe(switchMap((confirm: boolean) => {
|
||||
if (confirm) {
|
||||
const startScriptSucceeded$ = this.startScriptNotifyAndRedirect(dso, dso.handle);
|
||||
return startScriptSucceeded$.pipe(
|
||||
switchMap((r: boolean) => {
|
||||
return observableOf(r);
|
||||
})
|
||||
)
|
||||
} else {
|
||||
const modalRefExport = this.modalService.open(ExportMetadataSelectorComponent);
|
||||
modalRefExport.componentInstance.dsoRD = createSuccessfulRemoteDataObject(dso);
|
||||
}
|
||||
}));
|
||||
resp$.subscribe();
|
||||
return resp$;
|
||||
} else {
|
||||
this.notificationsService.error(this.translationService.get('dso-selector.export-metadata.notValidDSO'));
|
||||
return observableOf(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start export-metadata script of dso & navigate to process if successful
|
||||
* Otherwise show error message
|
||||
* @param dso Dso to export
|
||||
*/
|
||||
private startScriptNotifyAndRedirect(dso: DSpaceObject, handle: string): Observable<boolean> {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '-i', value: handle }),
|
||||
Object.assign(new ProcessParameter(), { name: '-f', value: dso.uuid + '.csv' }),
|
||||
];
|
||||
return this.scriptDataService.invoke(METADATA_EXPORT_SCRIPT_NAME, parameterValues, [])
|
||||
.pipe(
|
||||
take(1),
|
||||
map((requestEntry: RequestEntry) => {
|
||||
if (requestEntry.response.isSuccessful) {
|
||||
const title = this.translationService.get('process.new.notification.success.title');
|
||||
const content = this.translationService.get('process.new.notification.success.content');
|
||||
this.notificationsService.success(title, content);
|
||||
const response: any = requestEntry.response;
|
||||
if (isNotEmpty(response.resourceSelfLinks)) {
|
||||
const processNumber = response.resourceSelfLinks[0].split('/').pop();
|
||||
this.router.navigateByUrl('/processes/' + processNumber);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
const title = this.translationService.get('process.new.notification.error.title');
|
||||
const content = this.translationService.get('process.new.notification.error.content');
|
||||
this.notificationsService.error(title, content);
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
<div ng2FileDrop
|
||||
class="ds-document-drop-zone position-fixed h-100 w-100"
|
||||
[class.ds-document-drop-zone-active]="(isOverDocumentDropZone | async)"
|
||||
[uploader]="uploader"
|
||||
(onFileDrop)="setFile($event)"
|
||||
(fileOver)="fileOverDocument($event)">
|
||||
</div>
|
||||
<div *ngIf="(isOverDocumentDropZone | async)"
|
||||
class="ds-document-drop-zone-inner position-fixed h-100 w-100 p-2">
|
||||
<div
|
||||
class="ds-document-drop-zone-inner-content position-relative d-flex flex-column justify-content-center text-center h-100 w-100">
|
||||
<p class="text-primary">{{ dropMessageLabel | translate}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="well ds-base-drop-zone mt-1 mb-3 text-muted p-2">
|
||||
<p class="text-center m-0 p-0 d-flex justify-content-center align-items-center"
|
||||
*ngIf="fileObject!=null"> {{ fileObject.name }} </p>
|
||||
<p class="text-center m-0 p-0 d-flex justify-content-center align-items-center">
|
||||
<span><i class="fas fa-cloud-upload"
|
||||
aria-hidden="true"></i> {{ (fileObject == null ? dropMessageLabel : dropMessageLabelReplacement) | translate}} {{'uploader.or' | translate}}</span>
|
||||
<label class="btn btn-link m-0 p-0 ml-1">
|
||||
<input class="form-control-file d-none" requireFile #file="ngModel" type="file" name="file-upload"
|
||||
id="file-upload"
|
||||
[ngModel]="fileObject" (ngModelChange)="setFile($event)">
|
||||
{{'uploader.browse' | translate}}
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
@@ -0,0 +1,92 @@
|
||||
import { Component, EventEmitter, HostListener, Input, OnInit, Output } from '@angular/core';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { FileUploader } from 'ng2-file-upload';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { UploaderOptions } from '../uploader/uploader-options.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Component to have a file dropzone without that dropping/choosing a file in browse automatically triggers
|
||||
* the uploader, instead an event is emitted with the file that was added.
|
||||
*
|
||||
* Here only one file is allowed to be selected, so if one is selected/dropped the message changes to a
|
||||
* replace message.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-file-dropzone-no-uploader',
|
||||
templateUrl: './file-dropzone-no-uploader.component.html',
|
||||
styleUrls: ['./file-dropzone-no-uploader.scss']
|
||||
})
|
||||
export class FileDropzoneNoUploaderComponent implements OnInit {
|
||||
|
||||
public isOverDocumentDropZone: Observable<boolean>;
|
||||
public uploader: FileUploader;
|
||||
public uploaderId: string;
|
||||
|
||||
@Input() dropMessageLabel: string;
|
||||
@Input() dropMessageLabelReplacement: string;
|
||||
|
||||
/**
|
||||
* The function to call when file is added
|
||||
*/
|
||||
@Output() onFileAdded: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* The uploader configuration options
|
||||
* @type {UploaderOptions}
|
||||
*/
|
||||
uploadFilesOptions: UploaderOptions = Object.assign(new UploaderOptions(), {
|
||||
// URL needs to contain something to not produce any errors. We are using onFileDrop; not the uploader
|
||||
url: 'placeholder',
|
||||
});
|
||||
|
||||
/**
|
||||
* The current value of the file
|
||||
*/
|
||||
fileObject: File;
|
||||
|
||||
/**
|
||||
* Method provided by Angular. Invoked after the constructor.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.uploaderId = 'ds-drag-and-drop-uploader' + uniqueId();
|
||||
this.isOverDocumentDropZone = observableOf(false);
|
||||
this.uploader = new FileUploader({
|
||||
// required, but using onFileDrop, not uploader
|
||||
url: 'placeholder',
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('window:drop', ['$event'])
|
||||
onDrop(event: any) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
@HostListener('window:dragover', ['$event'])
|
||||
onDragOver(event: any) {
|
||||
// Show drop area on the page
|
||||
event.preventDefault();
|
||||
if ((event.target as any).tagName !== 'HTML') {
|
||||
this.isOverDocumentDropZone = observableOf(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when files are dragged on the window document drop area.
|
||||
*/
|
||||
public fileOverDocument(isOver: boolean) {
|
||||
if (!isOver) {
|
||||
this.isOverDocumentDropZone = observableOf(isOver);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set file
|
||||
* @param files
|
||||
*/
|
||||
setFile(files) {
|
||||
this.fileObject = files.length > 0 ? files[0] : undefined;
|
||||
this.onFileAdded.emit(this.fileObject);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
.ds-base-drop-zone {
|
||||
border: 2px dashed $gray-600;
|
||||
}
|
||||
|
||||
.ds-document-drop-zone {
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.ds-document-drop-zone-active {
|
||||
z-index: $drop-zone-area-z-index !important;
|
||||
}
|
||||
|
||||
.ds-document-drop-zone-inner {
|
||||
background-color: rgba($white, 0.7);
|
||||
z-index: $drop-zone-area-inner-z-index;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.ds-document-drop-zone-inner-content {
|
||||
border: 4px dashed map-get($theme-colors, primary);
|
||||
z-index: $drop-zone-area-inner-z-index;
|
||||
}
|
||||
|
||||
.ds-document-drop-zone-inner-content p {
|
||||
font-size: ($font-size-lg * 2.5);
|
||||
}
|
@@ -12,6 +12,7 @@ import { AuthService } from '../../../core/auth/auth.service';
|
||||
import { AuthMethod } from '../../../core/auth/models/auth.method';
|
||||
import { AuthServiceStub } from '../../testing/auth-service.stub';
|
||||
import { createTestComponent } from '../../testing/utils.test';
|
||||
import { HardRedirectService } from '../../../core/services/hard-redirect.service';
|
||||
|
||||
describe('LogInContainerComponent', () => {
|
||||
|
||||
@@ -20,7 +21,13 @@ describe('LogInContainerComponent', () => {
|
||||
|
||||
const authMethod = new AuthMethod('password');
|
||||
|
||||
let hardRedirectService: HardRedirectService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
|
||||
redirect: {},
|
||||
getCurrentRoute: {}
|
||||
});
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@@ -35,6 +42,7 @@ describe('LogInContainerComponent', () => {
|
||||
],
|
||||
providers: [
|
||||
{provide: AuthService, useClass: AuthServiceStub},
|
||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||
LogInContainerComponent
|
||||
],
|
||||
schemas: [
|
||||
|
@@ -18,6 +18,7 @@ import { NativeWindowService } from '../../core/services/window.service';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { createTestComponent } from '../testing/utils.test';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { HardRedirectService } from '../../core/services/hard-redirect.service';
|
||||
|
||||
describe('LogInComponent', () => {
|
||||
|
||||
@@ -33,8 +34,13 @@ describe('LogInComponent', () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
let hardRedirectService: HardRedirectService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
|
||||
redirect: {},
|
||||
getCurrentRoute: {}
|
||||
});
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@@ -58,6 +64,7 @@ describe('LogInComponent', () => {
|
||||
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||
// { provide: Router, useValue: new RouterStub() },
|
||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||
provideMockStore({ initialState }),
|
||||
LogInComponent
|
||||
],
|
||||
|
@@ -1,13 +1,9 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, takeWhile, } from 'rxjs/operators';
|
||||
import { select, Store } from '@ngrx/store';
|
||||
|
||||
import { AuthMethod } from '../../core/auth/models/auth.method';
|
||||
import { getAuthenticationMethods, isAuthenticated, isAuthenticationLoading } from '../../core/auth/selectors';
|
||||
import { CoreState } from '../../core/core.reducers';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { getForgotPasswordPath, getRegisterPath } from '../../app-routing.module';
|
||||
|
||||
/**
|
||||
@@ -19,7 +15,7 @@ import { getForgotPasswordPath, getRegisterPath } from '../../app-routing.module
|
||||
templateUrl: './log-in.component.html',
|
||||
styleUrls: ['./log-in.component.scss']
|
||||
})
|
||||
export class LogInComponent implements OnInit, OnDestroy {
|
||||
export class LogInComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* A boolean representing if LogInComponent is in a standalone page
|
||||
@@ -45,14 +41,7 @@ export class LogInComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
public loading: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Component state.
|
||||
* @type {boolean}
|
||||
*/
|
||||
private alive = true;
|
||||
|
||||
constructor(private store: Store<CoreState>,
|
||||
private authService: AuthService,) {
|
||||
constructor(private store: Store<CoreState>) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -66,21 +55,6 @@ export class LogInComponent implements OnInit, OnDestroy {
|
||||
|
||||
// set isAuthenticated
|
||||
this.isAuthenticated = this.store.pipe(select(isAuthenticated));
|
||||
|
||||
// subscribe to success
|
||||
this.store.pipe(
|
||||
select(isAuthenticated),
|
||||
takeWhile(() => this.alive),
|
||||
filter((authenticated) => authenticated))
|
||||
.subscribe(() => {
|
||||
this.authService.redirectAfterLoginSuccess(this.isStandalonePage);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.alive = false;
|
||||
}
|
||||
|
||||
getRegisterPath() {
|
||||
|
@@ -15,6 +15,7 @@ import { AuthServiceStub } from '../../../testing/auth-service.stub';
|
||||
import { AppState } from '../../../../app.reducer';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||
|
||||
describe('LogInPasswordComponent', () => {
|
||||
|
||||
@@ -29,8 +30,14 @@ describe('LogInPasswordComponent', () => {
|
||||
loading: false,
|
||||
};
|
||||
|
||||
let hardRedirectService: HardRedirectService;
|
||||
|
||||
beforeEach(() => {
|
||||
user = EPersonMock;
|
||||
|
||||
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
|
||||
getCurrentRoute: {}
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
@@ -47,7 +54,8 @@ describe('LogInPasswordComponent', () => {
|
||||
],
|
||||
providers: [
|
||||
{ provide: AuthService, useClass: AuthServiceStub },
|
||||
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Password) }
|
||||
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Password) },
|
||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
|
@@ -13,6 +13,8 @@ import { fadeOut } from '../../../animations/fade';
|
||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||
import { renderAuthMethodFor } from '../log-in.methods-decorator';
|
||||
import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
||||
import { AuthService } from '../../../../core/auth/auth.service';
|
||||
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||
|
||||
/**
|
||||
* /users/sign-in
|
||||
@@ -66,11 +68,15 @@ export class LogInPasswordComponent implements OnInit {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {AuthMethod} injectedAuthMethodModel
|
||||
* @param {AuthService} authService
|
||||
* @param {HardRedirectService} hardRedirectService
|
||||
* @param {FormBuilder} formBuilder
|
||||
* @param {Store<State>} store
|
||||
*/
|
||||
constructor(
|
||||
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
|
||||
private authService: AuthService,
|
||||
private hardRedirectService: HardRedirectService,
|
||||
private formBuilder: FormBuilder,
|
||||
private store: Store<CoreState>
|
||||
) {
|
||||
@@ -134,6 +140,8 @@ export class LogInPasswordComponent implements OnInit {
|
||||
email.trim();
|
||||
password.trim();
|
||||
|
||||
this.authService.setRedirectUrlIfNotSet(this.hardRedirectService.getCurrentRoute());
|
||||
|
||||
// dispatch AuthenticationAction
|
||||
this.store.dispatch(new AuthenticateAction(email, password));
|
||||
|
||||
|
@@ -17,6 +17,7 @@ import { NativeWindowService } from '../../../../core/services/window.service';
|
||||
import { RouterStub } from '../../../testing/router.stub';
|
||||
import { ActivatedRouteStub } from '../../../testing/active-router.stub';
|
||||
import { NativeWindowMockFactory } from '../../../mocks/mock-native-window-ref';
|
||||
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||
|
||||
describe('LogInShibbolethComponent', () => {
|
||||
|
||||
@@ -30,6 +31,7 @@ describe('LogInShibbolethComponent', () => {
|
||||
let location;
|
||||
|
||||
let authState;
|
||||
let hardRedirectService: HardRedirectService;
|
||||
|
||||
beforeEach(() => {
|
||||
user = EPersonMock;
|
||||
@@ -41,6 +43,10 @@ describe('LogInShibbolethComponent', () => {
|
||||
loaded: false,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
|
||||
getCurrentRoute: {}
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
@@ -59,6 +65,7 @@ describe('LogInShibbolethComponent', () => {
|
||||
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
|
@@ -12,6 +12,8 @@ import { isAuthenticated, isAuthenticationLoading } from '../../../../core/auth/
|
||||
import { RouteService } from '../../../../core/services/route.service';
|
||||
import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service';
|
||||
import { isNotNull } from '../../../empty.util';
|
||||
import { AuthService } from '../../../../core/auth/auth.service';
|
||||
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-log-in-shibboleth',
|
||||
@@ -51,12 +53,16 @@ export class LogInShibbolethComponent implements OnInit {
|
||||
* @param {AuthMethod} injectedAuthMethodModel
|
||||
* @param {NativeWindowRef} _window
|
||||
* @param {RouteService} route
|
||||
* @param {AuthService} authService
|
||||
* @param {HardRedirectService} hardRedirectService
|
||||
* @param {Store<State>} store
|
||||
*/
|
||||
constructor(
|
||||
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
|
||||
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||
private route: RouteService,
|
||||
private authService: AuthService,
|
||||
private hardRedirectService: HardRedirectService,
|
||||
private store: Store<CoreState>
|
||||
) {
|
||||
this.authMethod = injectedAuthMethodModel;
|
||||
@@ -75,6 +81,7 @@ export class LogInShibbolethComponent implements OnInit {
|
||||
}
|
||||
|
||||
redirectToShibboleth() {
|
||||
this.authService.setRedirectUrlIfNotSet(this.hardRedirectService.getCurrentRoute())
|
||||
let newLocationUrl = this.location;
|
||||
const currentUrl = this._window.nativeWindow.location.href;
|
||||
const myRegexp = /\?redirectUrl=(.*)/g;
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import { Component, Injector } from '@angular/core';
|
||||
import { Component, Injector, OnInit, OnDestroy } from '@angular/core';
|
||||
import { MenuService } from '../menu.service';
|
||||
import { MenuSection } from '../menu.reducer';
|
||||
import { getComponentForMenuItemType } from '../menu-item.decorator';
|
||||
import { MenuID, MenuItemType } from '../initial-menus-state';
|
||||
import { hasNoValue } from '../../empty.util';
|
||||
import { hasNoValue, hasValue } from '../../empty.util';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { MenuItemModel } from '../menu-item/models/menu-item.model';
|
||||
import { distinctUntilChanged } from 'rxjs/operators';
|
||||
import { distinctUntilChanged, switchMap } from 'rxjs/operators';
|
||||
import { GenericConstructor } from '../../../core/shared/generic-constructor';
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
/**
|
||||
* A basic implementation of a menu section's component
|
||||
@@ -16,7 +18,7 @@ import { GenericConstructor } from '../../../core/shared/generic-constructor';
|
||||
selector: 'ds-menu-section',
|
||||
template: ''
|
||||
})
|
||||
export class MenuSectionComponent {
|
||||
export class MenuSectionComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* Observable that emits whether or not this section is currently active
|
||||
@@ -28,20 +30,24 @@ export class MenuSectionComponent {
|
||||
*/
|
||||
menuID: MenuID;
|
||||
|
||||
/**
|
||||
* List of Injectors for each dynamically rendered menu item of this section
|
||||
*/
|
||||
itemInjectors: Map<string, Injector> = new Map<string, Injector>();
|
||||
|
||||
/**
|
||||
* List of child Components for each dynamically rendered menu item of this section
|
||||
*/
|
||||
itemComponents: Map<string, GenericConstructor<MenuSectionComponent>> = new Map<string, GenericConstructor<MenuSectionComponent>>();
|
||||
|
||||
/**
|
||||
* List of available subsections in this section
|
||||
*/
|
||||
subSections: Observable<MenuSection[]>;
|
||||
subSections$: Observable<MenuSection[]>;
|
||||
|
||||
/**
|
||||
* Map of components and injectors for each dynamically rendered menu section
|
||||
*/
|
||||
sectionMap$: BehaviorSubject<Map<string, {
|
||||
injector: Injector,
|
||||
component: GenericConstructor<MenuSectionComponent>
|
||||
}>> = new BehaviorSubject(new Map());
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
*/
|
||||
subs: Subscription[] = [];
|
||||
|
||||
constructor(public section: MenuSection, protected menuService: MenuService, protected injector: Injector) {
|
||||
}
|
||||
@@ -85,15 +91,34 @@ export class MenuSectionComponent {
|
||||
* Method for initializing all injectors and component constructors for the menu items in this section
|
||||
*/
|
||||
private initializeInjectorData() {
|
||||
this.itemInjectors.set(this.section.id, this.getItemModelInjector(this.section.model));
|
||||
this.itemComponents.set(this.section.id, this.getMenuItemComponent(this.section.model));
|
||||
this.subSections = this.menuService.getSubSectionsByParentID(this.menuID, this.section.id);
|
||||
this.subSections.subscribe((sections: MenuSection[]) => {
|
||||
sections.forEach((section: MenuSection) => {
|
||||
this.itemInjectors.set(section.id, this.getItemModelInjector(section.model));
|
||||
this.itemComponents.set(section.id, this.getMenuItemComponent(section.model));
|
||||
})
|
||||
this.updateSectionMap(
|
||||
this.section.id,
|
||||
this.getItemModelInjector(this.section.model),
|
||||
this.getMenuItemComponent(this.section.model)
|
||||
);
|
||||
this.subSections$ = this.menuService.getSubSectionsByParentID(this.menuID, this.section.id);
|
||||
this.subs.push(
|
||||
this.subSections$.pipe(
|
||||
// if you return an array from a switchMap it will emit each element as a separate event.
|
||||
// So this switchMap is equivalent to a subscribe with a forEach inside
|
||||
switchMap((sections: MenuSection[]) => sections)
|
||||
).subscribe((section: MenuSection) => {
|
||||
this.updateSectionMap(
|
||||
section.id,
|
||||
this.getItemModelInjector(section.model),
|
||||
this.getMenuItemComponent(section.model)
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the sectionMap
|
||||
*/
|
||||
private updateSectionMap(id, injector, component) {
|
||||
const nextMap = this.sectionMap$.getValue();
|
||||
nextMap.set(id, { injector, component });
|
||||
this.sectionMap$.next(nextMap);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,4 +149,12 @@ export class MenuSectionComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from open subscriptions
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.subs
|
||||
.filter((subscription) => hasValue(subscription))
|
||||
.forEach((subscription) => subscription.unsubscribe());
|
||||
}
|
||||
}
|
||||
|
@@ -3,12 +3,14 @@ import { Observable } from 'rxjs/internal/Observable';
|
||||
import { MenuService } from './menu.service';
|
||||
import { MenuID } from './initial-menus-state';
|
||||
import { MenuSection } from './menu.reducer';
|
||||
import { distinctUntilChanged, first, map } from 'rxjs/operators';
|
||||
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
import { hasValue } from '../empty.util';
|
||||
import { MenuSectionComponent } from './menu-section/menu-section.component';
|
||||
import { getComponentForMenu } from './menu-section.decorator';
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { compareArraysUsingIds } from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||
|
||||
/**
|
||||
* A basic implementation of a MenuComponent
|
||||
@@ -44,14 +46,12 @@ export class MenuComponent implements OnInit, OnDestroy {
|
||||
sections: Observable<MenuSection[]>;
|
||||
|
||||
/**
|
||||
* List of Injectors for each dynamically rendered menu section
|
||||
* Map of components and injectors for each dynamically rendered menu section
|
||||
*/
|
||||
sectionInjectors: Map<string, Injector> = new Map<string, Injector>();
|
||||
|
||||
/**
|
||||
* List of child Components for each dynamically rendered menu section
|
||||
*/
|
||||
sectionComponents: Map<string, GenericConstructor<MenuSectionComponent>> = new Map<string, GenericConstructor<MenuSectionComponent>>();
|
||||
sectionMap$: BehaviorSubject<Map<string, {
|
||||
injector: Injector,
|
||||
component: GenericConstructor<MenuSectionComponent>
|
||||
}>> = new BehaviorSubject(new Map());
|
||||
|
||||
/**
|
||||
* Prevent unnecessary rerendering
|
||||
@@ -79,13 +79,25 @@ export class MenuComponent implements OnInit, OnDestroy {
|
||||
this.menuCollapsed = this.menuService.isMenuCollapsed(this.menuID);
|
||||
this.menuPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
||||
this.menuVisible = this.menuService.isMenuVisible(this.menuID);
|
||||
this.sections = this.menuService.getMenuTopSections(this.menuID).pipe(distinctUntilChanged((x, y) => JSON.stringify(x) === JSON.stringify(y)));
|
||||
this.subs.push(this.sections.subscribe((sections: MenuSection[]) => {
|
||||
sections.forEach((section: MenuSection) => {
|
||||
this.sectionInjectors.set(section.id, this.getSectionDataInjector(section));
|
||||
this.getSectionComponent(section).pipe(first()).subscribe((constr) => this.sectionComponents.set(section.id, constr));
|
||||
this.sections = this.menuService.getMenuTopSections(this.menuID).pipe(distinctUntilChanged(compareArraysUsingIds()));
|
||||
this.subs.push(
|
||||
this.sections.pipe(
|
||||
// if you return an array from a switchMap it will emit each element as a separate event.
|
||||
// So this switchMap is equivalent to a subscribe with a forEach inside
|
||||
switchMap((sections: MenuSection[]) => sections),
|
||||
switchMap((section: MenuSection) => this.getSectionComponent(section).pipe(
|
||||
map((component: GenericConstructor<MenuSectionComponent>) => ({ section, component }))
|
||||
)),
|
||||
distinctUntilChanged((x,y) => x.section.id === y.section.id)
|
||||
).subscribe(({ section, component}) => {
|
||||
const nextMap = this.sectionMap$.getValue();
|
||||
nextMap.set(section.id, {
|
||||
injector: this.getSectionDataInjector(section),
|
||||
component
|
||||
});
|
||||
}));
|
||||
this.sectionMap$.next(nextMap);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -10,6 +10,9 @@ import { MissingTranslationHandler, TranslateModule } from '@ngx-translate/core'
|
||||
|
||||
import { NgxPaginationModule } from 'ngx-pagination';
|
||||
import { ComcolRoleComponent } from './comcol-forms/edit-comcol-page/comcol-role/comcol-role.component';
|
||||
import { ConfirmationModalComponent } from './confirmation-modal/confirmation-modal.component';
|
||||
import { ExportMetadataSelectorComponent } from './dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
|
||||
import { FileDropzoneNoUploaderComponent } from './file-dropzone-no-uploader/file-dropzone-no-uploader.component';
|
||||
import { PublicationListElementComponent } from './object-list/item-list-element/item-types/publication/publication-list-element.component';
|
||||
|
||||
import { FileUploadModule } from 'ng2-file-upload';
|
||||
@@ -307,6 +310,7 @@ const COMPONENTS = [
|
||||
ThumbnailComponent,
|
||||
GridThumbnailComponent,
|
||||
UploaderComponent,
|
||||
FileDropzoneNoUploaderComponent,
|
||||
ItemListPreviewComponent,
|
||||
MyDSpaceItemStatusComponent,
|
||||
ItemSubmitterComponent,
|
||||
@@ -397,7 +401,9 @@ const COMPONENTS = [
|
||||
EpersonSearchBoxComponent,
|
||||
GroupSearchBoxComponent,
|
||||
FileDownloadLinkComponent,
|
||||
CollectionDropdownComponent
|
||||
CollectionDropdownComponent,
|
||||
ExportMetadataSelectorComponent,
|
||||
ConfirmationModalComponent
|
||||
];
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
@@ -474,6 +480,8 @@ const ENTRY_COMPONENTS = [
|
||||
ClaimedTaskActionsEditMetadataComponent,
|
||||
FileDownloadLinkComponent,
|
||||
CurationFormComponent,
|
||||
ExportMetadataSelectorComponent,
|
||||
ConfirmationModalComponent
|
||||
];
|
||||
|
||||
const SHARED_ITEM_PAGE_COMPONENTS = [
|
||||
|
@@ -154,4 +154,12 @@ export class AuthServiceStub {
|
||||
resetAuthenticationError() {
|
||||
return;
|
||||
}
|
||||
|
||||
setRedirectUrlIfNotSet(url: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
redirectAfterLoginSuccess() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@@ -442,6 +442,26 @@
|
||||
|
||||
|
||||
|
||||
"admin.metadata-import.breadcrumbs": "Import Metadata",
|
||||
|
||||
"admin.metadata-import.title": "Import Metadata",
|
||||
|
||||
"admin.metadata-import.page.header": "Import Metadata",
|
||||
|
||||
"admin.metadata-import.page.help": "You can drop or browse CSV files that contain batch metadata operations on files here",
|
||||
|
||||
"admin.metadata-import.page.dropMsg": "Drop a metadata CSV to import",
|
||||
|
||||
"admin.metadata-import.page.dropMsgReplace": "Drop to replace the metadata CSV to import",
|
||||
|
||||
"admin.metadata-import.page.button.return": "Return",
|
||||
|
||||
"admin.metadata-import.page.button.proceed": "Proceed",
|
||||
|
||||
"admin.metadata-import.page.error.addFile": "Select file first!",
|
||||
|
||||
|
||||
|
||||
|
||||
"auth.errors.invalid-user": "Invalid email address or password.",
|
||||
|
||||
@@ -967,12 +987,26 @@
|
||||
|
||||
"dso-selector.edit.item.head": "Edit item",
|
||||
|
||||
"dso-selector.export-metadata.dspaceobject.head": "Export metadata from",
|
||||
|
||||
"dso-selector.export-metadata.notValidDSO": "You can only export metadata for collections and communities",
|
||||
|
||||
"dso-selector.no-results": "No {{ type }} found",
|
||||
|
||||
"dso-selector.placeholder": "Search for a {{ type }}",
|
||||
|
||||
|
||||
|
||||
"confirmation-modal.export-metadata.header": "Export metadata for {{ dsoName }}",
|
||||
|
||||
"confirmation-modal.export-metadata.info": "Are you sure you want to export metadata for {{ dsoName }}",
|
||||
|
||||
"confirmation-modal.export-metadata.cancel": "Cancel",
|
||||
|
||||
"confirmation-modal.export-metadata.confirm": "Export",
|
||||
|
||||
|
||||
|
||||
"error.bitstream": "Error fetching bitstream",
|
||||
|
||||
"error.browse-by": "Error fetching items",
|
||||
@@ -1590,6 +1624,13 @@
|
||||
|
||||
"item.page.uri": "URI",
|
||||
|
||||
"item.page.bitstreams.view-more": "Show more",
|
||||
|
||||
"item.page.bitstreams.collapse": "Collapse",
|
||||
|
||||
"item.page.filesection.original.bundle" : "Original bundle",
|
||||
|
||||
"item.page.filesection.license.bundle" : "License bundle",
|
||||
|
||||
|
||||
"item.select.confirm": "Confirm selected",
|
||||
@@ -3118,6 +3159,6 @@
|
||||
|
||||
"workflow-item.send-back.button.cancel": "Cancel",
|
||||
|
||||
"workflow-item.send-back.button.confirm": "Send back",
|
||||
"workflow-item.send-back.button.confirm": "Send back"
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { HttpClient, HttpClientModule } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { InjectionToken, NgModule } from '@angular/core';
|
||||
import { BrowserModule, makeStateKey, TransferState } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterModule } from '@angular/router';
|
||||
@@ -21,6 +21,8 @@ import { AuthService } from '../../app/core/auth/auth.service';
|
||||
import { Angulartics2RouterlessModule } from 'angulartics2/routerlessmodule';
|
||||
import { SubmissionService } from '../../app/submission/submission.service';
|
||||
import { StatisticsModule } from '../../app/statistics/statistics.module';
|
||||
import { HardRedirectService } from '../../app/core/services/hard-redirect.service';
|
||||
import { BrowserHardRedirectService } from '../../app/core/services/browser-hard-redirect.service';
|
||||
|
||||
export const REQ_KEY = makeStateKey<string>('req');
|
||||
|
||||
@@ -32,6 +34,12 @@ export function getRequest(transferState: TransferState): any {
|
||||
return transferState.get<any>(REQ_KEY, {});
|
||||
}
|
||||
|
||||
export const LocationToken = new InjectionToken('Location');
|
||||
|
||||
export function locationProvider(): Location {
|
||||
return window.location;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
bootstrap: [AppComponent],
|
||||
imports: [
|
||||
@@ -78,7 +86,15 @@ export function getRequest(transferState: TransferState): any {
|
||||
{
|
||||
provide: SubmissionService,
|
||||
useClass: SubmissionService
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: HardRedirectService,
|
||||
useClass: BrowserHardRedirectService,
|
||||
},
|
||||
{
|
||||
provide: LocationToken,
|
||||
useFactory: locationProvider,
|
||||
},
|
||||
]
|
||||
})
|
||||
export class BrowserAppModule {
|
||||
|
@@ -29,6 +29,8 @@ import { ServerLocaleService } from 'src/app/core/locale/server-locale.service';
|
||||
import { LocaleService } from 'src/app/core/locale/locale.service';
|
||||
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { ForwardClientIpInterceptor } from '../../app/core/forward-client-ip/forward-client-ip.interceptor';
|
||||
import { HardRedirectService } from '../../app/core/services/hard-redirect.service';
|
||||
import { ServerHardRedirectService } from '../../app/core/services/server-hard-redirect.service';
|
||||
|
||||
export function createTranslateLoader() {
|
||||
return new TranslateJson5UniversalLoader('dist/server/assets/i18n/', '.json5');
|
||||
@@ -88,6 +90,10 @@ export function createTranslateLoader() {
|
||||
useClass: ForwardClientIpInterceptor,
|
||||
multi: true
|
||||
},
|
||||
{
|
||||
provide: HardRedirectService,
|
||||
useClass: ServerHardRedirectService,
|
||||
},
|
||||
]
|
||||
})
|
||||
export class ServerAppModule {
|
||||
|
151
src/robots.txt
Normal file
151
src/robots.txt
Normal file
@@ -0,0 +1,151 @@
|
||||
# The URL to the DSpace sitemaps
|
||||
# XML sitemap is listed first as it is preferred by most search engines
|
||||
Sitemap: /sitemap_index.xml
|
||||
Sitemap: /sitemap_index.html
|
||||
|
||||
##########################
|
||||
# Default Access Group
|
||||
# (NOTE: blank lines are not allowable in a group record)
|
||||
##########################
|
||||
User-agent: *
|
||||
# Disable access to Discovery search and filters; admin pages; processes; submission; workspace; workflow & profile page
|
||||
Disallow: /search
|
||||
Disallow: /admin/*
|
||||
Disallow: /processes
|
||||
Disallow: /submit
|
||||
Disallow: /workspaceitems
|
||||
Disallow: /profile
|
||||
Disallow: /workflowitems
|
||||
|
||||
# Optionally uncomment the following line ONLY if sitemaps are working
|
||||
# and you have verified that your site is being indexed correctly.
|
||||
# Disallow: /browse/*
|
||||
#
|
||||
# If you have configured DSpace (Solr-based) Statistics to be publicly
|
||||
# accessible, then you may not want this content to be indexed
|
||||
# Disallow: /statistics
|
||||
#
|
||||
# You also may wish to disallow access to the following paths, in order
|
||||
# to stop web spiders from accessing user-based content
|
||||
# Disallow: /contact
|
||||
# Disallow: /feedback
|
||||
# Disallow: /forgot
|
||||
# Disallow: /login
|
||||
# Disallow: /register
|
||||
|
||||
|
||||
##############################
|
||||
# Section for misbehaving bots
|
||||
# The following directives to block specific robots were borrowed from Wikipedia's robots.txt
|
||||
##############################
|
||||
|
||||
# advertising-related bots:
|
||||
User-agent: Mediapartners-Google*
|
||||
Disallow: /
|
||||
|
||||
# Crawlers that are kind enough to obey, but which we'd rather not have
|
||||
# unless they're feeding search engines.
|
||||
User-agent: UbiCrawler
|
||||
Disallow: /
|
||||
|
||||
User-agent: DOC
|
||||
Disallow: /
|
||||
|
||||
User-agent: Zao
|
||||
Disallow: /
|
||||
|
||||
# Some bots are known to be trouble, particularly those designed to copy
|
||||
# entire sites. Please obey robots.txt.
|
||||
User-agent: sitecheck.internetseer.com
|
||||
Disallow: /
|
||||
|
||||
User-agent: Zealbot
|
||||
Disallow: /
|
||||
|
||||
User-agent: MSIECrawler
|
||||
Disallow: /
|
||||
|
||||
User-agent: SiteSnagger
|
||||
Disallow: /
|
||||
|
||||
User-agent: WebStripper
|
||||
Disallow: /
|
||||
|
||||
User-agent: WebCopier
|
||||
Disallow: /
|
||||
|
||||
User-agent: Fetch
|
||||
Disallow: /
|
||||
|
||||
User-agent: Offline Explorer
|
||||
Disallow: /
|
||||
|
||||
User-agent: Teleport
|
||||
Disallow: /
|
||||
|
||||
User-agent: TeleportPro
|
||||
Disallow: /
|
||||
|
||||
User-agent: WebZIP
|
||||
Disallow: /
|
||||
|
||||
User-agent: linko
|
||||
Disallow: /
|
||||
|
||||
User-agent: HTTrack
|
||||
Disallow: /
|
||||
|
||||
User-agent: Microsoft.URL.Control
|
||||
Disallow: /
|
||||
|
||||
User-agent: Xenu
|
||||
Disallow: /
|
||||
|
||||
User-agent: larbin
|
||||
Disallow: /
|
||||
|
||||
User-agent: libwww
|
||||
Disallow: /
|
||||
|
||||
User-agent: ZyBORG
|
||||
Disallow: /
|
||||
|
||||
User-agent: Download Ninja
|
||||
Disallow: /
|
||||
|
||||
# Misbehaving: requests much too fast:
|
||||
User-agent: fast
|
||||
Disallow: /
|
||||
|
||||
#
|
||||
# If your DSpace is going down because of someone using recursive wget,
|
||||
# you can activate the following rule.
|
||||
#
|
||||
# If your own faculty is bringing down your dspace with recursive wget,
|
||||
# you can advise them to use the --wait option to set the delay between hits.
|
||||
#
|
||||
#User-agent: wget
|
||||
#Disallow: /
|
||||
|
||||
#
|
||||
# The 'grub' distributed client has been *very* poorly behaved.
|
||||
#
|
||||
User-agent: grub-client
|
||||
Disallow: /
|
||||
|
||||
#
|
||||
# Doesn't follow robots.txt anyway, but...
|
||||
#
|
||||
User-agent: k2spider
|
||||
Disallow: /
|
||||
|
||||
#
|
||||
# Hits many times per second, not acceptable
|
||||
# http://www.nameprotect.com/botinfo.html
|
||||
User-agent: NPBot
|
||||
Disallow: /
|
||||
|
||||
# A capture bot, downloads gazillions of pages with no public benefit
|
||||
# http://www.webreaper.net/
|
||||
User-agent: WebReaper
|
||||
Disallow: /
|
@@ -1,45 +0,0 @@
|
||||
const webpackMerge = require('webpack-merge');
|
||||
|
||||
const prodPartial = require('./webpack/webpack.prod');
|
||||
|
||||
module.exports = (env, options) => {
|
||||
env = env || {};
|
||||
const commonPartial = require('./webpack/webpack.common')(env);
|
||||
const clientPartial = require('./webpack/webpack.client')(env);
|
||||
const { getAotPlugin } = require('./webpack/webpack.aot')(env);
|
||||
const { getServerWebpackPartial } = require('./webpack/webpack.server')(env);
|
||||
|
||||
if (env.aot) {
|
||||
console.log(`Running build for ${env.client ? 'client' : 'server'} with AoT Compilation`);
|
||||
}
|
||||
|
||||
const serverPartial = getServerWebpackPartial(env.aot);
|
||||
|
||||
let serverConfig = webpackMerge({}, commonPartial, serverPartial, {
|
||||
plugins: [
|
||||
getAotPlugin('server', !!env.aot)
|
||||
]
|
||||
});
|
||||
|
||||
let clientConfig = webpackMerge({}, commonPartial, clientPartial, {
|
||||
plugins: [
|
||||
getAotPlugin('client', !!env.aot)
|
||||
]
|
||||
});
|
||||
if (options.mode === 'production') {
|
||||
serverConfig = webpackMerge({}, serverConfig, prodPartial);
|
||||
clientConfig = webpackMerge({}, clientConfig, prodPartial);
|
||||
}
|
||||
|
||||
const configs = [];
|
||||
|
||||
if (!env.aot) {
|
||||
configs.push(clientConfig, serverConfig);
|
||||
} else if (env.client) {
|
||||
configs.push(clientConfig);
|
||||
} else if (env.server) {
|
||||
configs.push(serverConfig);
|
||||
}
|
||||
|
||||
return configs;
|
||||
};
|
8
webpack/webpack.browser.ts
Normal file
8
webpack/webpack.browser.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { commonExports } from './webpack.common';
|
||||
|
||||
module.exports = Object.assign({}, commonExports, {
|
||||
target: 'web',
|
||||
node: {
|
||||
module: 'empty'
|
||||
}
|
||||
});
|
@@ -21,11 +21,13 @@ export const copyWebpackOptions = [
|
||||
}, {
|
||||
from: path.join(__dirname, '..', 'src', 'assets', 'i18n'),
|
||||
to: path.join('assets', 'i18n')
|
||||
}, {
|
||||
from: path.join(__dirname, '..', 'src', 'robots.txt'),
|
||||
to: path.join('robots.txt')
|
||||
}
|
||||
];
|
||||
|
||||
export const commonExports = {
|
||||
target: 'web',
|
||||
plugins: [
|
||||
new CopyWebpackPlugin(copyWebpackOptions),
|
||||
new HtmlWebpackPlugin({
|
||||
@@ -37,9 +39,6 @@ export const commonExports = {
|
||||
defaultAttribute: 'defer'
|
||||
})
|
||||
],
|
||||
node: {
|
||||
module: 'empty'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@@ -99,5 +98,3 @@ export const commonExports = {
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = commonExports;
|
||||
|
@@ -1,48 +1,12 @@
|
||||
import { commonExports } from './webpack.common';
|
||||
import { buildRoot, projectRoot } from './helpers';
|
||||
|
||||
const webpack = require('webpack');
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
const CompressionPlugin = require('compression-webpack-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
const cssnano = require('cssnano');
|
||||
const nodeExternals = require('webpack-node-externals');
|
||||
|
||||
import { buildRoot, globalCSSImports, projectRoot, theme, themedTest, themedUse, themePath } from './helpers';
|
||||
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const ScriptExtPlugin = require('script-ext-html-webpack-plugin');
|
||||
|
||||
export const copyWebpackOptions = [
|
||||
{
|
||||
from: path.join(__dirname, '..', 'node_modules', '@fortawesome', 'fontawesome-free', 'webfonts'),
|
||||
to: path.join('assets', 'fonts'),
|
||||
force: undefined
|
||||
},
|
||||
{
|
||||
from: path.join(__dirname, '..', 'src', 'assets', 'fonts'),
|
||||
to: path.join('assets', 'fonts')
|
||||
}, {
|
||||
from: path.join(__dirname, '..', 'src', 'assets', 'images'),
|
||||
to: path.join('assets', 'images')
|
||||
}, {
|
||||
from: path.join(__dirname, '..', 'src', 'assets', 'i18n'),
|
||||
to: path.join('assets', 'i18n')
|
||||
}
|
||||
];
|
||||
|
||||
export const commonExports = {
|
||||
module.exports = Object.assign({}, commonExports, {
|
||||
plugins: [
|
||||
new CopyWebpackPlugin(copyWebpackOptions),
|
||||
new HtmlWebpackPlugin({
|
||||
template: buildRoot('./index.html', ),
|
||||
output: projectRoot('dist'),
|
||||
inject: 'head'
|
||||
}),
|
||||
new ScriptExtPlugin({
|
||||
defaultAttribute: 'defer'
|
||||
}),
|
||||
...commonExports.plugins,
|
||||
new webpack.EnvironmentPlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: JSON.stringify('production'),
|
||||
@@ -50,64 +14,6 @@ export const commonExports = {
|
||||
}
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: (filePath) => themedTest(filePath, 'scss'),
|
||||
use: (info) => themedUse(info.resource, 'scss')
|
||||
},
|
||||
{
|
||||
test: (filePath) => themedTest(filePath, 'html'),
|
||||
use: (info) => themedUse(info.resource, 'html')
|
||||
},
|
||||
{
|
||||
test: /\.ts$/,
|
||||
loader: '@ngtools/webpack'
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
exclude: [
|
||||
/node_modules/,
|
||||
buildRoot('styles/_exposed_variables.scss'),
|
||||
buildRoot('styles/_variables.scss')
|
||||
],
|
||||
use: [
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
includePaths: [projectRoot('./'), path.join(themePath, 'styles')]
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'sass-resources-loader',
|
||||
options: {
|
||||
resources: globalCSSImports()
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /(_exposed)?_variables.scss$/,
|
||||
exclude: [/node_modules/],
|
||||
use: [
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
sourceMap: true
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
includePaths: [projectRoot('./'), path.join(themePath, 'styles')]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
mode: 'production',
|
||||
recordsOutputPath: projectRoot('webpack.records.json'),
|
||||
entry: buildRoot('./main.server.ts'),
|
||||
@@ -122,6 +28,4 @@ export const commonExports = {
|
||||
/sortablejs/,
|
||||
/ngx/]
|
||||
})],
|
||||
};
|
||||
|
||||
module.exports = commonExports;
|
||||
});
|
||||
|
8
webpack/webpack.test.ts
Normal file
8
webpack/webpack.test.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { commonExports } from './webpack.common';
|
||||
|
||||
module.exports = Object.assign({}, commonExports, {
|
||||
target: 'web',
|
||||
node: {
|
||||
module: 'empty'
|
||||
}
|
||||
});
|
35
yarn.lock
35
yarn.lock
@@ -1162,6 +1162,13 @@
|
||||
"@types/minimatch" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/http-proxy@^1.17.4":
|
||||
version "1.17.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b"
|
||||
integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/jasmine@*", "@types/jasmine@^3.3.9":
|
||||
version "3.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.5.7.tgz#c4237935a4b0db31ada6b0de10e2b0658e7b2643"
|
||||
@@ -2090,7 +2097,7 @@ braces@^2.3.1, braces@^2.3.2:
|
||||
split-string "^3.0.2"
|
||||
to-regex "^3.0.1"
|
||||
|
||||
braces@^3.0.2, braces@~3.0.2:
|
||||
braces@^3.0.1, braces@^3.0.2, braces@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
||||
@@ -5131,6 +5138,17 @@ http-proxy-middleware@0.19.1:
|
||||
lodash "^4.17.11"
|
||||
micromatch "^3.1.10"
|
||||
|
||||
http-proxy-middleware@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-1.0.5.tgz#4c6e25d95a411e3d750bc79ccf66290675176dc2"
|
||||
integrity sha512-CKzML7u4RdGob8wuKI//H8Ein6wNTEQR7yjVEzPbhBLGdOfkfvgTnp2HLnniKBDP9QW4eG10/724iTWLBeER3g==
|
||||
dependencies:
|
||||
"@types/http-proxy" "^1.17.4"
|
||||
http-proxy "^1.18.1"
|
||||
is-glob "^4.0.1"
|
||||
lodash "^4.17.19"
|
||||
micromatch "^4.0.2"
|
||||
|
||||
http-proxy@^1.17.0:
|
||||
version "1.18.0"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a"
|
||||
@@ -6324,6 +6342,11 @@ lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||
|
||||
lodash@^4.17.19:
|
||||
version "4.17.19"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
|
||||
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
|
||||
|
||||
log-driver@^1.2.7:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.7.tgz#63b95021f0702fedfa2c9bb0a24e7797d71871d8"
|
||||
@@ -6580,6 +6603,14 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4:
|
||||
snapdragon "^0.8.1"
|
||||
to-regex "^3.0.2"
|
||||
|
||||
micromatch@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
|
||||
integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
|
||||
dependencies:
|
||||
braces "^3.0.1"
|
||||
picomatch "^2.0.5"
|
||||
|
||||
miller-rabin@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
|
||||
@@ -7615,7 +7646,7 @@ picomatch@^2.0.4, picomatch@^2.0.7:
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a"
|
||||
integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==
|
||||
|
||||
picomatch@^2.2.1:
|
||||
picomatch@^2.0.5, picomatch@^2.2.1:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
|
||||
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
|
||||
|
Reference in New Issue
Block a user