mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge pull request #1642 from harvard-lts/145-base-path-support
[Issue 145] Support base path
This commit is contained in:
@@ -9,10 +9,11 @@
|
||||
"start:dev": "nodemon --exec \"cross-env NODE_ENV=development yarn run serve\"",
|
||||
"start:prod": "yarn run build:prod && cross-env NODE_ENV=production yarn run serve:ssr",
|
||||
"start:mirador:prod": "yarn run build:mirador && yarn run start:prod",
|
||||
"serve": "ng serve -c development",
|
||||
"preserve": "yarn base-href",
|
||||
"serve": "ng serve --configuration development",
|
||||
"serve:ssr": "node dist/server/main",
|
||||
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
|
||||
"build": "ng build -c development",
|
||||
"build": "ng build --configuration development",
|
||||
"build:stats": "ng build --stats-json",
|
||||
"build:prod": "yarn run build:ssr",
|
||||
"build:ssr": "ng build --configuration production && ng run dspace-angular:server:production",
|
||||
@@ -37,6 +38,7 @@
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:run": "cypress run",
|
||||
"env:yaml": "ts-node --project ./tsconfig.ts-node.json scripts/env-to-yaml.ts",
|
||||
"base-href": "ts-node --project ./tsconfig.ts-node.json scripts/base-href.ts",
|
||||
"check-circ-deps": "npx madge --exclude '(bitstream|bundle|collection|config-submission-form|eperson|item|version)\\.model\\.ts$' --circular --extensions ts ./"
|
||||
},
|
||||
"browser": {
|
||||
|
36
scripts/base-href.ts
Normal file
36
scripts/base-href.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as fs from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { AppConfig } from '../src/config/app-config.interface';
|
||||
import { buildAppConfig } from '../src/config/config.server';
|
||||
|
||||
/**
|
||||
* Script to set baseHref as `ui.nameSpace` for development mode. Adds `baseHref` to angular.json build options.
|
||||
*
|
||||
* Usage (see package.json):
|
||||
*
|
||||
* yarn base-href
|
||||
*/
|
||||
|
||||
const appConfig: AppConfig = buildAppConfig();
|
||||
|
||||
const angularJsonPath = join(process.cwd(), 'angular.json');
|
||||
|
||||
if (!fs.existsSync(angularJsonPath)) {
|
||||
console.error(`Error:\n${angularJsonPath} does not exist\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const angularJson = require(angularJsonPath);
|
||||
|
||||
const baseHref = `${appConfig.ui.nameSpace}${appConfig.ui.nameSpace.endsWith('/') ? '' : '/'}`;
|
||||
|
||||
console.log(`Setting baseHref to ${baseHref} in angular.json`);
|
||||
|
||||
angularJson.projects['dspace-angular'].architect.build.options.baseHref = baseHref;
|
||||
|
||||
fs.writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2) + '\n');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
32
server.ts
32
server.ts
@@ -67,6 +67,8 @@ extendEnvironmentWithAppConfig(environment, appConfig);
|
||||
// The Express app is exported so that it can be used by serverless Functions.
|
||||
export function app() {
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/*
|
||||
* Create a new express application
|
||||
*/
|
||||
@@ -138,7 +140,11 @@ export function app() {
|
||||
/**
|
||||
* Proxy the sitemaps
|
||||
*/
|
||||
server.use('/sitemap**', createProxyMiddleware({ target: `${environment.rest.baseUrl}/sitemaps`, changeOrigin: true }));
|
||||
router.use('/sitemap**', createProxyMiddleware({
|
||||
target: `${environment.rest.baseUrl}/sitemaps`,
|
||||
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
||||
changeOrigin: true
|
||||
}));
|
||||
|
||||
/**
|
||||
* Checks if the rateLimiter property is present
|
||||
@@ -157,7 +163,7 @@ export function app() {
|
||||
* Serve static resources (images, i18n messages, …)
|
||||
* Handle pre-compressed files with [express-static-gzip](https://github.com/tkoenig89/express-static-gzip)
|
||||
*/
|
||||
server.get('*.*', cacheControl, expressStaticGzip(DIST_FOLDER, {
|
||||
router.get('*.*', cacheControl, expressStaticGzip(DIST_FOLDER, {
|
||||
index: false,
|
||||
enableBrotli: true,
|
||||
orderPreference: ['br', 'gzip'],
|
||||
@@ -166,10 +172,12 @@ export function app() {
|
||||
/*
|
||||
* Fallthrough to the IIIF viewer (must be included in the build).
|
||||
*/
|
||||
server.use('/iiif', express.static(IIIF_VIEWER, {index:false}));
|
||||
router.use('/iiif', express.static(IIIF_VIEWER, { index: false }));
|
||||
|
||||
// Register the ngApp callback function to handle incoming requests
|
||||
server.get('*', ngApp);
|
||||
router.get('*', ngApp);
|
||||
|
||||
server.use(environment.ui.nameSpace, router);
|
||||
|
||||
return server;
|
||||
}
|
||||
@@ -203,13 +211,25 @@ function ngApp(req, res) {
|
||||
if (hasValue(err)) {
|
||||
console.warn('Error details : ', err);
|
||||
}
|
||||
res.sendFile(DIST_FOLDER + '/index.html');
|
||||
res.render(indexHtml, {
|
||||
req,
|
||||
providers: [{
|
||||
provide: APP_BASE_HREF,
|
||||
useValue: req.baseUrl
|
||||
}]
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If preboot is disabled, just serve the client
|
||||
console.log('Universal off, serving for direct CSR');
|
||||
res.sendFile(DIST_FOLDER + '/index.html');
|
||||
res.render(indexHtml, {
|
||||
req,
|
||||
providers: [{
|
||||
provide: APP_BASE_HREF,
|
||||
useValue: req.baseUrl
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -187,7 +187,7 @@ describe('App component', () => {
|
||||
link.setAttribute('rel', 'stylesheet');
|
||||
link.setAttribute('type', 'text/css');
|
||||
link.setAttribute('class', 'theme-css');
|
||||
link.setAttribute('href', '/custom-theme.css');
|
||||
link.setAttribute('href', 'custom-theme.css');
|
||||
|
||||
expect(headSpy.appendChild).toHaveBeenCalledWith(link);
|
||||
});
|
||||
|
@@ -268,7 +268,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
link.setAttribute('rel', 'stylesheet');
|
||||
link.setAttribute('type', 'text/css');
|
||||
link.setAttribute('class', 'theme-css');
|
||||
link.setAttribute('href', `/${encodeURIComponent(themeName)}-theme.css`);
|
||||
link.setAttribute('href', `${encodeURIComponent(themeName)}-theme.css`);
|
||||
// wait for the new css to download before removing the old one to prevent a
|
||||
// flash of unstyled content
|
||||
link.onload = () => {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { APP_BASE_HREF, CommonModule } from '@angular/common';
|
||||
import { APP_BASE_HREF, CommonModule, DOCUMENT } from '@angular/common';
|
||||
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
|
||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
import { AbstractControl } from '@angular/forms';
|
||||
@@ -42,9 +42,11 @@ export function getConfig() {
|
||||
return environment;
|
||||
}
|
||||
|
||||
export function getBase(appConfig: AppConfig) {
|
||||
return appConfig.ui.nameSpace;
|
||||
}
|
||||
const getBaseHref = (document: Document, appConfig: AppConfig): string => {
|
||||
const baseTag = document.querySelector('head > base');
|
||||
baseTag.setAttribute('href', `${appConfig.ui.nameSpace}${appConfig.ui.nameSpace.endsWith('/') ? '' : '/'}`);
|
||||
return baseTag.getAttribute('href');
|
||||
};
|
||||
|
||||
export function getMetaReducers(appConfig: AppConfig): MetaReducer<AppState>[] {
|
||||
return appConfig.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers;
|
||||
@@ -84,8 +86,8 @@ const PROVIDERS = [
|
||||
},
|
||||
{
|
||||
provide: APP_BASE_HREF,
|
||||
useFactory: getBase,
|
||||
deps: [APP_CONFIG]
|
||||
useFactory: getBaseHref,
|
||||
deps: [DOCUMENT, APP_CONFIG]
|
||||
},
|
||||
{
|
||||
provide: USER_PROVIDED_META_REDUCERS,
|
||||
|
@@ -377,25 +377,25 @@ describe('AuthService test', () => {
|
||||
it('should redirect to reload with redirect url', () => {
|
||||
authService.navigateToRedirectUrl('/collection/123');
|
||||
// Reload with redirect URL set to /collection/123
|
||||
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*\\?redirect=' + encodeURIComponent('/collection/123'))));
|
||||
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('reload/[0-9]*\\?redirect=' + encodeURIComponent('/collection/123'))));
|
||||
});
|
||||
|
||||
it('should redirect to reload with /home', () => {
|
||||
authService.navigateToRedirectUrl('/home');
|
||||
// Reload with redirect URL set to /home
|
||||
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*\\?redirect=' + encodeURIComponent('/home'))));
|
||||
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('reload/[0-9]*\\?redirect=' + encodeURIComponent('/home'))));
|
||||
});
|
||||
|
||||
it('should redirect to regular reload and not to /login', () => {
|
||||
authService.navigateToRedirectUrl('/login');
|
||||
// Reload without a redirect URL
|
||||
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*(?!\\?)$')));
|
||||
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('reload/[0-9]*(?!\\?)$')));
|
||||
});
|
||||
|
||||
it('should redirect to regular reload when no redirect url is found', () => {
|
||||
authService.navigateToRedirectUrl(undefined);
|
||||
// Reload without a redirect URL
|
||||
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('/reload/[0-9]*(?!\\?)$')));
|
||||
expect(hardRedirectService.redirect).toHaveBeenCalledWith(jasmine.stringMatching(new RegExp('reload/[0-9]*(?!\\?)$')));
|
||||
});
|
||||
|
||||
describe('impersonate', () => {
|
||||
|
@@ -468,8 +468,8 @@ export class AuthService {
|
||||
*/
|
||||
public navigateToRedirectUrl(redirectUrl: string) {
|
||||
// Don't do redirect if already on reload url
|
||||
if (!hasValue(redirectUrl) || !redirectUrl.includes('/reload/')) {
|
||||
let url = `/reload/${new Date().getTime()}`;
|
||||
if (!hasValue(redirectUrl) || !redirectUrl.includes('reload/')) {
|
||||
let url = `reload/${new Date().getTime()}`;
|
||||
if (isNotEmpty(redirectUrl) && !redirectUrl.startsWith(LOGIN_ROUTE)) {
|
||||
url += `?redirect=${encodeURIComponent(redirectUrl)}`;
|
||||
}
|
||||
|
@@ -192,7 +192,7 @@ export class LocaleService {
|
||||
this.routeService.getCurrentUrl().pipe(take(1)).subscribe((currentURL) => {
|
||||
// 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()}?redirect=` + encodeURIComponent(currentURL);
|
||||
this._window.nativeWindow.location.href = `reload/${new Date().getTime()}?redirect=` + encodeURIComponent(currentURL);
|
||||
});
|
||||
|
||||
}
|
||||
|
@@ -1,13 +1,17 @@
|
||||
import { ReloadGuard } from './reload.guard';
|
||||
import { Router } from '@angular/router';
|
||||
import { AppConfig } from '../../../config/app-config.interface';
|
||||
import { DefaultAppConfig } from '../../../config/default-app-config';
|
||||
import { ReloadGuard } from './reload.guard';
|
||||
|
||||
describe('ReloadGuard', () => {
|
||||
let guard: ReloadGuard;
|
||||
let router: Router;
|
||||
let appConfig: AppConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
router = jasmine.createSpyObj('router', ['parseUrl', 'createUrlTree']);
|
||||
guard = new ReloadGuard(router);
|
||||
appConfig = new DefaultAppConfig();
|
||||
guard = new ReloadGuard(router, appConfig);
|
||||
});
|
||||
|
||||
describe('canActivate', () => {
|
||||
@@ -27,7 +31,7 @@ describe('ReloadGuard', () => {
|
||||
|
||||
it('should create a UrlTree with the redirect URL', () => {
|
||||
guard.canActivate(route, undefined);
|
||||
expect(router.parseUrl).toHaveBeenCalledWith(redirectUrl);
|
||||
expect(router.parseUrl).toHaveBeenCalledWith(redirectUrl.substring(1));
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
|
||||
/**
|
||||
@@ -8,7 +9,10 @@ import { isNotEmpty } from '../../shared/empty.util';
|
||||
*/
|
||||
@Injectable()
|
||||
export class ReloadGuard implements CanActivate {
|
||||
constructor(private router: Router) {
|
||||
constructor(
|
||||
private router: Router,
|
||||
@Inject(APP_CONFIG) private appConfig: AppConfig,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -18,7 +22,10 @@ export class ReloadGuard implements CanActivate {
|
||||
*/
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): UrlTree {
|
||||
if (isNotEmpty(route.queryParams.redirect)) {
|
||||
return this.router.parseUrl(route.queryParams.redirect);
|
||||
const url = route.queryParams.redirect.startsWith(this.appConfig.ui.nameSpace)
|
||||
? route.queryParams.redirect.substring(this.appConfig.ui.nameSpace.length)
|
||||
: route.queryParams.redirect;
|
||||
return this.router.parseUrl(url);
|
||||
} else {
|
||||
return this.router.createUrlTree(['home']);
|
||||
}
|
||||
|
@@ -5,6 +5,6 @@
|
||||
<p>{{"500.help" | translate}}</p>
|
||||
<br/>
|
||||
<p class="text-center">
|
||||
<a href="/home" class="btn btn-primary">{{"500.link.home-page" | translate}}</a>
|
||||
<a href="home" class="btn btn-primary">{{"500.link.home-page" | translate}}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
@@ -4,7 +4,6 @@ import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { LinkMenuItemComponent } from './link-menu-item.component';
|
||||
import { RouterLinkDirectiveStub } from '../../testing/router-link-directive.stub';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { QueryParamsDirectiveStub } from '../../testing/query-params-directive.stub';
|
||||
import { RouterStub } from '../../testing/router.stub';
|
||||
import { Router } from '@angular/router';
|
||||
@@ -58,7 +57,7 @@ describe('LinkMenuItemComponent', () => {
|
||||
const routerLinkQuery = linkDes.map((de) => de.injector.get(RouterLinkDirectiveStub));
|
||||
|
||||
expect(routerLinkQuery.length).toBe(1);
|
||||
expect(routerLinkQuery[0].routerLink).toBe(environment.ui.nameSpace + link);
|
||||
expect(routerLinkQuery[0].routerLink).toBe(link);
|
||||
});
|
||||
|
||||
it('should have the right queryParams attribute', () => {
|
||||
|
@@ -2,7 +2,6 @@ import { Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { LinkMenuItemModel } from './models/link.model';
|
||||
import { rendersMenuItemForType } from '../menu-item.decorator';
|
||||
import { isNotEmpty } from '../../empty.util';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { MenuItemType } from '../menu-item-type.model';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@@ -30,7 +29,7 @@ export class LinkMenuItemComponent implements OnInit {
|
||||
|
||||
getRouterLink() {
|
||||
if (this.hasLink) {
|
||||
return environment.ui.nameSpace + this.item.link;
|
||||
return this.item.link;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
@@ -98,7 +98,7 @@ describe('NotificationComponent', () => {
|
||||
it('should have html content', () => {
|
||||
fixture = TestBed.createComponent(NotificationComponent);
|
||||
comp = fixture.componentInstance;
|
||||
const htmlContent = '<a class="btn btn-link p-0 m-0 pb-1" href="/test"><strong>test</strong></a>';
|
||||
const htmlContent = '<a class="btn btn-link p-0 m-0 pb-1" href="test"><strong>test</strong></a>';
|
||||
comp.notification = {
|
||||
id: '1',
|
||||
type: NotificationType.Info,
|
||||
|
@@ -13,6 +13,6 @@
|
||||
</body>
|
||||
|
||||
<!-- this is needed for CSR fallback -->
|
||||
<script async src="/client.js"></script>
|
||||
<script async src="client.js"></script>
|
||||
|
||||
</html>
|
||||
|
@@ -6,7 +6,9 @@ $sidebar-items-width: 250px !default;
|
||||
$total-sidebar-width: $collapsed-sidebar-width + $sidebar-items-width !default;
|
||||
|
||||
/* Fonts */
|
||||
$fa-font-path: "/assets/fonts" !default;
|
||||
// Starting this url with a caret (^) allows it to be a relative path based on UI's deployment path
|
||||
// See https://github.com/angular/angular-cli/issues/12797#issuecomment-598534241
|
||||
$fa-font-path: "^assets/fonts" !default;
|
||||
/* Images */
|
||||
$image-path: "../assets/images" !default;
|
||||
|
||||
|
Reference in New Issue
Block a user