Merge branch 'main-gh' into DSC-389

This commit is contained in:
Davide Negretti
2022-03-11 10:53:35 +01:00
38 changed files with 16209 additions and 15927 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

View File

@@ -101,7 +101,7 @@ Installing
### Configuring ### Configuring
Default configuration file is located in `config/` folder. Default runtime configuration file is located in `config/` folder. These configurations can be changed without rebuilding the distribution.
To override the default configuration values, create local files that override the parameters you need to change. You can use `config.example.yml` as a starting point. To override the default configuration values, create local files that override the parameters you need to change. You can use `config.example.yml` as a starting point.
@@ -167,6 +167,22 @@ These configuration sources are collected **at run time**, and written to `dist/
The configuration file can be externalized by using environment variable `DSPACE_APP_CONFIG_PATH`. The configuration file can be externalized by using environment variable `DSPACE_APP_CONFIG_PATH`.
#### Buildtime Configuring
Buildtime configuration must defined before build in order to include in transpiled JavaScript. This is primarily for the server. These settings can be found under `src/environment/` folder.
To override the default configuration values for development, create local file that override the build time parameters you need to change.
- Create a new `environment.(dev or development).ts` file in `src/environment/` for a `development` environment;
If needing to update default configurations values for production, update local file that override the build time parameters you need to change.
- Update `environment.production.ts` file in `src/environment/` for a `production` environment;
The environment object is provided for use as import in code and is extended with he runtime configuration on bootstrap of the application.
> Take caution moving runtime configs into the buildtime configuration. They will be overwritten by what is defined in the runtime config on bootstrap.
#### Using environment variables in code #### Using environment variables in code
To use environment variables in a UI component, use: To use environment variables in a UI component, use:
@@ -183,7 +199,6 @@ or
import { environment } from '../environment.ts'; import { environment } from '../environment.ts';
``` ```
Running the app Running the app
--------------- ---------------

View File

@@ -1,93 +1,93 @@
# Docker Compose files # Docker Compose files
*** ***
:warning: **NOT PRODUCTION READY** The below Docker Compose resources are not guaranteed "production ready" at this time. They have been built for development/testing only. Therefore, DSpace Docker images may not be fully secured or up-to-date. While you are welcome to base your own images on these DSpace images/resources, these should not be used "as is" in any production scenario. :warning: **NOT PRODUCTION READY** The below Docker Compose resources are not guaranteed "production ready" at this time. They have been built for development/testing only. Therefore, DSpace Docker images may not be fully secured or up-to-date. While you are welcome to base your own images on these DSpace images/resources, these should not be used "as is" in any production scenario.
*** ***
## 'Dockerfile' in root directory ## 'Dockerfile' in root directory
This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular' This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular'
``` ```
docker build -t dspace/dspace-angular:dspace-7_x . docker build -t dspace/dspace-angular:dspace-7_x .
``` ```
This image is built *automatically* after each commit is made to the `main` branch. This image is built *automatically* after each commit is made to the `main` branch.
Admins to our DockerHub repo can manually publish with the following command. Admins to our DockerHub repo can manually publish with the following command.
``` ```
docker push dspace/dspace-angular:dspace-7_x docker push dspace/dspace-angular:dspace-7_x
``` ```
## docker directory ## docker directory
- docker-compose.yml - docker-compose.yml
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker. - Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.
- docker-compose-rest.yml - docker-compose-rest.yml
- Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes - Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes
- docker-compose-ci.yml - docker-compose-ci.yml
- Runs a published instance of the DSpace 7 REST API for CI testing. The database is re-populated from a SQL dump on each startup. - Runs a published instance of the DSpace 7 REST API for CI testing. The database is re-populated from a SQL dump on each startup.
- cli.yml - cli.yml
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container. - Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
- cli.assetstore.yml - cli.assetstore.yml
- Docker compose file that will download and install data into a DSpace REST assetstore. This script points to a default dataset that will be utilized for CI testing. - Docker compose file that will download and install data into a DSpace REST assetstore. This script points to a default dataset that will be utilized for CI testing.
## To refresh / pull DSpace images from Dockerhub ## To refresh / pull DSpace images from Dockerhub
``` ```
docker-compose -f docker/docker-compose.yml pull docker-compose -f docker/docker-compose.yml pull
``` ```
## To build DSpace images using code in your branch ## To build DSpace images using code in your branch
``` ```
docker-compose -f docker/docker-compose.yml build docker-compose -f docker/docker-compose.yml build
``` ```
## To start DSpace (REST and Angular) from your branch ## To start DSpace (REST and Angular) from your branch
``` ```
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d
``` ```
## Run DSpace REST and DSpace Angular from local branches. ## Run DSpace REST and DSpace Angular from local branches.
_The system will be started in 2 steps. Each step shares the same docker network._ _The system will be started in 2 steps. Each step shares the same docker network._
From DSpace/DSpace (build as needed) From DSpace/DSpace (build as needed)
``` ```
docker-compose -p d7 up -d docker-compose -p d7 up -d
``` ```
From DSpace/DSpace-angular From DSpace/DSpace-angular
``` ```
docker-compose -p d7 -f docker/docker-compose.yml up -d docker-compose -p d7 -f docker/docker-compose.yml up -d
``` ```
## Ingest test data from AIPDIR ## Ingest test data from AIPDIR
Create an administrator Create an administrator
``` ```
docker-compose -p d7 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en docker-compose -p d7 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en
``` ```
Load content from AIP files Load content from AIP files
``` ```
docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
``` ```
## Alternative Ingest - Use Entities dataset ## Alternative Ingest - Use Entities dataset
_Delete your docker volumes or use a unique project (-p) name_ _Delete your docker volumes or use a unique project (-p) name_
Start DSpace with Database Content from a database dump Start DSpace with Database Content from a database dump
``` ```
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d
``` ```
Load assetstore content and trigger a re-index of the repository Load assetstore content and trigger a re-index of the repository
``` ```
docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
``` ```
## End to end testing of the rest api (runs in travis). ## End to end testing of the rest api (runs in travis).
_In this instance, only the REST api runs in Docker using the Entities dataset. Travis will perform CI testing of Angular using Node to drive the tests._ _In this instance, only the REST api runs in Docker using the Entities dataset. Travis will perform CI testing of Angular using Node to drive the tests._
``` ```
docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d
``` ```

View File

@@ -116,7 +116,7 @@
"rxjs": "^6.6.3", "rxjs": "^6.6.3",
"sortablejs": "1.13.0", "sortablejs": "1.13.0",
"tslib": "^2.0.0", "tslib": "^2.0.0",
"url-parse": "^1.5.3", "url-parse": "^1.5.6",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"webfontloader": "1.6.28", "webfontloader": "1.6.28",
"zone.js": "^0.10.3" "zone.js": "^0.10.3"
@@ -158,7 +158,7 @@
"jasmine-core": "~3.6.0", "jasmine-core": "~3.6.0",
"jasmine-marbles": "0.6.0", "jasmine-marbles": "0.6.0",
"jasmine-spec-reporter": "~5.0.0", "jasmine-spec-reporter": "~5.0.0",
"karma": "^5.2.3", "karma": "^6.3.14",
"karma-chrome-launcher": "~3.1.0", "karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2", "karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0", "karma-jasmine": "~4.0.0",

View File

@@ -18,6 +18,7 @@ import { PaginationService } from '../../core/pagination/pagination.service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { isValidDate } from '../../shared/date.util';
@Component({ @Component({
selector: 'ds-browse-by-date-page', selector: 'ds-browse-by-date-page',
@@ -85,10 +86,10 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
let lowerLimit = environment.browseBy.defaultLowerLimit; let lowerLimit = environment.browseBy.defaultLowerLimit;
if (hasValue(firstItemRD.payload)) { if (hasValue(firstItemRD.payload)) {
const date = firstItemRD.payload.firstMetadataValue(metadataKeys); const date = firstItemRD.payload.firstMetadataValue(metadataKeys);
if (hasValue(date)) { if (isNotEmpty(date) && isValidDate(date)) {
const dateObj = new Date(date); const dateObj = new Date(date);
// TODO: it appears that getFullYear (based on local time) is sometimes unreliable. Switching to UTC. // TODO: it appears that getFullYear (based on local time) is sometimes unreliable. Switching to UTC.
lowerLimit = dateObj.getUTCFullYear(); lowerLimit = isNaN(dateObj.getUTCFullYear()) ? lowerLimit : dateObj.getUTCFullYear();
} }
} }
const options = []; const options = [];

View File

@@ -219,6 +219,9 @@ describe('AuthEffects', () => {
const expected = cold('--b-', { b: new RetrieveTokenAction() }); const expected = cold('--b-', { b: new RetrieveTokenAction() });
expect(authEffects.checkTokenCookie$).toBeObservable(expected); expect(authEffects.checkTokenCookie$).toBeObservable(expected);
authEffects.checkTokenCookie$.subscribe(() => {
expect((authEffects as any).authorizationsService.invalidateAuthorizationsRequestCache).toHaveBeenCalled();
});
}); });
it('should return a RETRIEVE_AUTH_METHODS action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is false', () => { it('should return a RETRIEVE_AUTH_METHODS action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is false', () => {

View File

@@ -162,6 +162,7 @@ export class AuthEffects {
return this.authService.checkAuthenticationCookie().pipe( return this.authService.checkAuthenticationCookie().pipe(
map((response: AuthStatus) => { map((response: AuthStatus) => {
if (response.authenticated) { if (response.authenticated) {
this.authorizationsService.invalidateAuthorizationsRequestCache();
return new RetrieveTokenAction(); return new RetrieveTokenAction();
} else { } else {
return new RetrieveAuthMethodsAction(response); return new RetrieveAuthMethodsAction(response);

View File

@@ -3,7 +3,7 @@ import { compare } from 'fast-json-patch';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { getClassForType } from '../cache/builders/build-decorators'; import { getClassForType } from '../cache/builders/build-decorators';
import { TypedObject } from '../cache/object-cache.reducer'; import { TypedObject } from '../cache/object-cache.reducer';
import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { DSpaceNotNullSerializer } from '../dspace-rest/dspace-not-null.serializer';
import { ChangeAnalyzer } from './change-analyzer'; import { ChangeAnalyzer } from './change-analyzer';
/** /**
@@ -22,8 +22,8 @@ export class DefaultChangeAnalyzer<T extends TypedObject> implements ChangeAnaly
* The second object to compare * The second object to compare
*/ */
diff(object1: T, object2: T): Operation[] { diff(object1: T, object2: T): Operation[] {
const serializer1 = new DSpaceSerializer(getClassForType(object1.type)); const serializer1 = new DSpaceNotNullSerializer(getClassForType(object1.type));
const serializer2 = new DSpaceSerializer(getClassForType(object2.type)); const serializer2 = new DSpaceNotNullSerializer(getClassForType(object2.type));
return compare(serializer1.serialize(object1), serializer2.serialize(object2)); return compare(serializer1.serialize(object1), serializer2.serialize(object2));
} }
} }

View File

@@ -90,10 +90,12 @@ describe('EpersonRegistrationService', () => {
const expected = service.searchByToken('test-token'); const expected = service.searchByToken('test-token');
expect(expected).toBeObservable(cold('(a|)', { expect(expected).toBeObservable(cold('(a|)', {
a: Object.assign(new Registration(), { a: jasmine.objectContaining({
email: registrationWithUser.email, payload: Object.assign(new Registration(), {
token: 'test-token', email: registrationWithUser.email,
user: registrationWithUser.user token: 'test-token',
user: registrationWithUser.user
})
}) })
})); }));
}); });

View File

@@ -79,7 +79,7 @@ export class EpersonRegistrationService {
* Search a registration based on the provided token * Search a registration based on the provided token
* @param token * @param token
*/ */
searchByToken(token: string): Observable<Registration> { searchByToken(token: string): Observable<RemoteData<Registration>> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();
const href$ = this.getTokenSearchEndpoint(token).pipe( const href$ = this.getTokenSearchEndpoint(token).pipe(
@@ -97,15 +97,14 @@ export class EpersonRegistrationService {
}); });
return this.rdbService.buildSingle<Registration>(href$).pipe( return this.rdbService.buildSingle<Registration>(href$).pipe(
skipWhile((rd: RemoteData<Registration>) => rd.isStale), map((rd) => {
getFirstSucceededRemoteData(), if (rd.hasSucceeded && hasValue(rd.payload)) {
map((restResponse: RemoteData<Registration>) => { return Object.assign(rd, { payload: Object.assign(rd.payload, { token }) });
return Object.assign(new Registration(), { } else {
email: restResponse.payload.email, token: token, user: restResponse.payload.user return rd;
}); }
}), })
); );
} }
} }

View File

@@ -0,0 +1,76 @@
import { Deserialize, Serialize } from 'cerialize';
import { Serializer } from '../serializer';
import { GenericConstructor } from '../shared/generic-constructor';
/**
* This Serializer turns responses from DSpace's REST API
* to models and vice versa, but with all fields with null value removed for the Serialized objects
*/
export class DSpaceNotNullSerializer<T> implements Serializer<T> {
/**
* Create a new DSpaceNotNullSerializer instance
*
* @param modelType a class or interface to indicate
* the kind of model this serializer should work with
*/
constructor(private modelType: GenericConstructor<T>) {
}
/**
* Convert a model in to the format expected by the backend, but with all fields with null value removed
*
* @param model The model to serialize
* @returns An object to send to the backend
*/
serialize(model: T): any {
return getSerializedObjectWithoutNullFields(Serialize(model, this.modelType));
}
/**
* Convert an array of models in to the format expected by the backend, but with all fields with null value removed
*
* @param models The array of models to serialize
* @returns An object to send to the backend
*/
serializeArray(models: T[]): any {
return getSerializedObjectWithoutNullFields(Serialize(models, this.modelType));
}
/**
* Convert a response from the backend in to a model.
*
* @param response An object returned by the backend
* @returns a model of type T
*/
deserialize(response: any): T {
if (Array.isArray(response)) {
throw new Error('Expected a single model, use deserializeArray() instead');
}
return Deserialize(response, this.modelType) as T;
}
/**
* Convert a response from the backend in to an array of models
*
* @param response An object returned by the backend
* @returns an array of models of type T
*/
deserializeArray(response: any): T[] {
if (!Array.isArray(response)) {
throw new Error('Expected an Array, use deserialize() instead');
}
return Deserialize(response, this.modelType) as T[];
}
}
function getSerializedObjectWithoutNullFields(serializedObjectBefore): any {
const copySerializedObject = {};
for (const [key, value] of Object.entries(serializedObjectBefore)) {
if (value !== null) {
copySerializedObject[key] = value;
}
}
return copySerializedObject;
}

View File

@@ -1,4 +1,4 @@
<div class="container"> <div class="container" *ngIf="(registration$ |async)">
<h3 class="mb-4">{{'forgot-password.form.head' | translate}}</h3> <h3 class="mb-4">{{'forgot-password.form.head' | translate}}</h3>
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header">{{'forgot-password.form.identification.header' | translate}}</div> <div class="card-header">{{'forgot-password.form.identification.header' | translate}}</div>
@@ -33,4 +33,4 @@
(click)="submit()">{{'forgot-password.form.submit' | translate}}</button> (click)="submit()">{{'forgot-password.form.submit' | translate}}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -16,7 +16,11 @@ import { Registration } from '../../core/shared/registration.model';
import { ForgotPasswordFormComponent } from './forgot-password-form.component'; import { ForgotPasswordFormComponent } from './forgot-password-form.component';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { AuthenticateAction } from '../../core/auth/auth.actions'; import { AuthenticateAction } from '../../core/auth/auth.actions';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$
} from '../../shared/remote-data.utils';
describe('ForgotPasswordFormComponent', () => { describe('ForgotPasswordFormComponent', () => {
let comp: ForgotPasswordFormComponent; let comp: ForgotPasswordFormComponent;
@@ -36,7 +40,7 @@ describe('ForgotPasswordFormComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
route = {data: observableOf({registration: registration})}; route = {data: observableOf({registration: createSuccessfulRemoteDataObject(registration)})};
router = new RouterStub(); router = new RouterStub();
notificationsService = new NotificationsServiceStub(); notificationsService = new NotificationsServiceStub();

View File

@@ -11,7 +11,10 @@ import { Store } from '@ngrx/store';
import { CoreState } from '../../core/core.reducers'; import { CoreState } from '../../core/core.reducers';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import {
getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload,
} from '../../core/shared/operators';
@Component({ @Component({
selector: 'ds-forgot-password-form', selector: 'ds-forgot-password-form',
@@ -48,7 +51,8 @@ export class ForgotPasswordFormComponent {
ngOnInit(): void { ngOnInit(): void {
this.registration$ = this.route.data.pipe( this.registration$ = this.route.data.pipe(
map((data) => data.registration as Registration), map((data) => data.registration as RemoteData<Registration>),
getFirstSucceededRemoteDataPayload(),
); );
this.registration$.subscribe((registration: Registration) => { this.registration$.subscribe((registration: Registration) => {
this.email = registration.email; this.email = registration.email;

View File

@@ -1,9 +1,9 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { ItemPageResolver } from '../item-page/item-page.resolver'; import { ItemPageResolver } from '../item-page/item-page.resolver';
import { RegistrationResolver } from '../register-email-form/registration.resolver';
import { ThemedForgotPasswordFormComponent } from './forgot-password-form/themed-forgot-password-form.component'; import { ThemedForgotPasswordFormComponent } from './forgot-password-form/themed-forgot-password-form.component';
import { ThemedForgotEmailComponent } from './forgot-password-email/themed-forgot-email.component'; import { ThemedForgotEmailComponent } from './forgot-password-email/themed-forgot-email.component';
import { RegistrationGuard } from '../register-page/registration.guard';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -16,12 +16,11 @@ import { ThemedForgotEmailComponent } from './forgot-password-email/themed-forgo
{ {
path: ':token', path: ':token',
component: ThemedForgotPasswordFormComponent, component: ThemedForgotPasswordFormComponent,
resolve: {registration: RegistrationResolver} canActivate: [ RegistrationGuard ],
} }
]) ])
], ],
providers: [ providers: [
RegistrationResolver,
ItemPageResolver, ItemPageResolver,
] ]
}) })

View File

@@ -1,8 +1,8 @@
import { RegistrationResolver } from './registration.resolver'; import { RegistrationResolver } from './registration.resolver';
import { EpersonRegistrationService } from '../core/data/eperson-registration.service'; import { EpersonRegistrationService } from '../core/data/eperson-registration.service';
import { of as observableOf } from 'rxjs';
import { Registration } from '../core/shared/registration.model'; import { Registration } from '../core/shared/registration.model';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
describe('RegistrationResolver', () => { describe('RegistrationResolver', () => {
let resolver: RegistrationResolver; let resolver: RegistrationResolver;
@@ -13,7 +13,7 @@ describe('RegistrationResolver', () => {
beforeEach(() => { beforeEach(() => {
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
searchByToken: observableOf(registration) searchByToken: createSuccessfulRemoteDataObject$(registration)
}); });
resolver = new RegistrationResolver(epersonRegistrationService); resolver = new RegistrationResolver(epersonRegistrationService);
}); });
@@ -23,9 +23,9 @@ describe('RegistrationResolver', () => {
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(resolved) => { (resolved) => {
expect(resolved.token).toEqual(token); expect(resolved.payload.token).toEqual(token);
expect(resolved.email).toEqual('test@email.org'); expect(resolved.payload.email).toEqual('test@email.org');
expect(resolved.user).toEqual('user-uuid'); expect(resolved.payload.user).toEqual('user-uuid');
done(); done();
} }
); );

View File

@@ -3,18 +3,22 @@ import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/r
import { EpersonRegistrationService } from '../core/data/eperson-registration.service'; import { EpersonRegistrationService } from '../core/data/eperson-registration.service';
import { Registration } from '../core/shared/registration.model'; import { Registration } from '../core/shared/registration.model';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { RemoteData } from '../core/data/remote-data';
import { getFirstCompletedRemoteData } from '../core/shared/operators';
@Injectable() @Injectable()
/** /**
* Resolver to resolve a Registration object based on the provided token * Resolver to resolve a Registration object based on the provided token
*/ */
export class RegistrationResolver implements Resolve<Registration> { export class RegistrationResolver implements Resolve<RemoteData<Registration>> {
constructor(private epersonRegistrationService: EpersonRegistrationService) { constructor(private epersonRegistrationService: EpersonRegistrationService) {
} }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Registration> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Registration>> {
const token = route.params.token; const token = route.params.token;
return this.epersonRegistrationService.searchByToken(token); return this.epersonRegistrationService.searchByToken(token).pipe(
getFirstCompletedRemoteData(),
);
} }
} }

View File

@@ -1,4 +1,4 @@
<div class="container"> <div class="container" *ngIf="(registration$ |async)">
<h3 class="mb-4">{{'register-page.create-profile.header' | translate}}</h3> <h3 class="mb-4">{{'register-page.create-profile.header' | translate}}</h3>
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header">{{'register-page.create-profile.identification.header' | translate}}</div> <div class="card-header">{{'register-page.create-profile.identification.header' | translate}}</div>

View File

@@ -21,7 +21,11 @@ import {
END_USER_AGREEMENT_METADATA_FIELD, END_USER_AGREEMENT_METADATA_FIELD,
EndUserAgreementService EndUserAgreementService
} from '../../core/end-user-agreement/end-user-agreement.service'; } from '../../core/end-user-agreement/end-user-agreement.service';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$
} from '../../shared/remote-data.utils';
describe('CreateProfileComponent', () => { describe('CreateProfileComponent', () => {
let comp: CreateProfileComponent; let comp: CreateProfileComponent;
@@ -106,7 +110,7 @@ describe('CreateProfileComponent', () => {
}; };
epersonWithAgreement = Object.assign(new EPerson(), valuesWithAgreement); epersonWithAgreement = Object.assign(new EPerson(), valuesWithAgreement);
route = {data: observableOf({registration: registration})}; route = {data: observableOf({registration: createSuccessfulRemoteDataObject(registration)})};
router = new RouterStub(); router = new RouterStub();
notificationsService = new NotificationsServiceStub(); notificationsService = new NotificationsServiceStub();

View File

@@ -19,7 +19,7 @@ import {
END_USER_AGREEMENT_METADATA_FIELD, END_USER_AGREEMENT_METADATA_FIELD,
EndUserAgreementService EndUserAgreementService
} from '../../core/end-user-agreement/end-user-agreement.service'; } from '../../core/end-user-agreement/end-user-agreement.service';
import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
/** /**
* Component that renders the create profile page to be used by a user registering through a token * Component that renders the create profile page to be used by a user registering through a token
@@ -56,7 +56,8 @@ export class CreateProfileComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.registration$ = this.route.data.pipe( this.registration$ = this.route.data.pipe(
map((data) => data.registration as Registration), map((data) => data.registration as RemoteData<Registration>),
getFirstSucceededRemoteDataPayload(),
); );
this.registration$.subscribe((registration: Registration) => { this.registration$.subscribe((registration: Registration) => {
this.email = registration.email; this.email = registration.email;

View File

@@ -2,9 +2,9 @@ import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { RegisterEmailComponent } from './register-email/register-email.component'; import { RegisterEmailComponent } from './register-email/register-email.component';
import { ItemPageResolver } from '../item-page/item-page.resolver'; import { ItemPageResolver } from '../item-page/item-page.resolver';
import { RegistrationResolver } from '../register-email-form/registration.resolver';
import { EndUserAgreementCookieGuard } from '../core/end-user-agreement/end-user-agreement-cookie.guard'; import { EndUserAgreementCookieGuard } from '../core/end-user-agreement/end-user-agreement-cookie.guard';
import { ThemedCreateProfileComponent } from './create-profile/themed-create-profile.component'; import { ThemedCreateProfileComponent } from './create-profile/themed-create-profile.component';
import { RegistrationGuard } from './registration.guard';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -17,13 +17,14 @@ import { ThemedCreateProfileComponent } from './create-profile/themed-create-pro
{ {
path: ':token', path: ':token',
component: ThemedCreateProfileComponent, component: ThemedCreateProfileComponent,
resolve: {registration: RegistrationResolver}, canActivate: [
canActivate: [EndUserAgreementCookieGuard] RegistrationGuard,
EndUserAgreementCookieGuard,
],
} }
]) ])
], ],
providers: [ providers: [
RegistrationResolver,
ItemPageResolver ItemPageResolver
] ]
}) })

View File

@@ -0,0 +1,106 @@
import { RegistrationGuard } from './registration.guard';
import { EpersonRegistrationService } from '../core/data/eperson-registration.service';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../core/auth/auth.service';
import { Location } from '@angular/common';
import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject,
} from '../shared/remote-data.utils';
import { Registration } from '../core/shared/registration.model';
import { of as observableOf } from 'rxjs/internal/observable/of';
import { RemoteData } from '../core/data/remote-data';
describe('RegistrationGuard', () => {
let guard: RegistrationGuard;
let epersonRegistrationService: EpersonRegistrationService;
let router: Router;
let authService: AuthService;
let registration: Registration;
let registrationRD: RemoteData<Registration>;
let currentUrl: string;
let startingRouteData: any;
let route: ActivatedRouteSnapshot;
let state: RouterStateSnapshot;
beforeEach(() => {
registration = Object.assign(new Registration(), {
email: 'test@email.com',
token: 'testToken',
user: 'testUser',
});
registrationRD = createSuccessfulRemoteDataObject(registration);
currentUrl = 'test-current-url';
startingRouteData = {
existingData: 'some-existing-data',
};
route = Object.assign(new ActivatedRouteSnapshot(), {
data: Object.assign({}, startingRouteData),
params: {
token: 'testToken',
},
});
state = Object.assign({
url: currentUrl,
});
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
searchByToken: observableOf(registrationRD),
});
router = jasmine.createSpyObj('router', {
navigateByUrl: Promise.resolve(),
}, {
url: currentUrl,
});
authService = jasmine.createSpyObj('authService', {
isAuthenticated: observableOf(false),
setRedirectUrl: {},
});
guard = new RegistrationGuard(epersonRegistrationService, router, authService);
});
describe('canActivate', () => {
describe('when searchByToken returns a successful response', () => {
beforeEach(() => {
(epersonRegistrationService.searchByToken as jasmine.Spy).and.returnValue(observableOf(registrationRD));
});
it('should return true', (done) => {
guard.canActivate(route, state).subscribe((result) => {
expect(result).toEqual(true);
done();
});
});
it('should add the response to the route\'s data', (done) => {
guard.canActivate(route, state).subscribe(() => {
expect(route.data).toEqual({ ...startingRouteData, registration: registrationRD });
done();
});
});
it('should not redirect', (done) => {
guard.canActivate(route, state).subscribe(() => {
expect(router.navigateByUrl).not.toHaveBeenCalled();
done();
});
});
});
describe('when searchByToken returns a 404 response', () => {
beforeEach(() => {
(epersonRegistrationService.searchByToken as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Not Found', 404));
});
it('should redirect', () => {
guard.canActivate(route, state).subscribe();
expect(router.navigateByUrl).toHaveBeenCalled();
});
});
});
});

View File

@@ -0,0 +1,43 @@
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { EpersonRegistrationService } from '../core/data/eperson-registration.service';
import { AuthService } from '../core/auth/auth.service';
import { map } from 'rxjs/operators';
import { getFirstCompletedRemoteData, redirectOn4xx } from '../core/shared/operators';
import { Location } from '@angular/common';
@Injectable({
providedIn: 'root'
})
/**
* A guard responsible for redirecting to 4xx pages upon retrieving a Registration object
* The guard also adds the resulting RemoteData<Registration> object to the route's data for further usage in components
* The reason this is a guard and not a resolver, is because it has to run before the EndUserAgreementCookieGuard
*/
export class RegistrationGuard implements CanActivate {
constructor(private epersonRegistrationService: EpersonRegistrationService,
private router: Router,
private authService: AuthService) {
}
/**
* Can the user activate the route? Returns true if the provided token resolves to an existing Registration, false if
* not. Redirects to 4xx page on 4xx error. Adds the resulting RemoteData<Registration> object to the route's
* data.registration property
* @param route
* @param state
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const token = route.params.token;
return this.epersonRegistrationService.searchByToken(token).pipe(
getFirstCompletedRemoteData(),
redirectOn4xx(this.router, this.authService),
map((rd) => {
route.data = { ...route.data, registration: rd };
return rd.hasSucceeded;
}),
);
}
}

View File

@@ -1,14 +1,10 @@
<div class="outer-wrapper" *ngIf="!shouldShowFullscreenLoader; else fullScreenLoader"> <div class="outer-wrapper" *ngIf="!shouldShowFullscreenLoader; else fullScreenLoader">
<ds-themed-admin-sidebar></ds-themed-admin-sidebar> <ds-themed-admin-sidebar></ds-themed-admin-sidebar>
<div class="inner-wrapper" [@slideSidebarPadding]="{ <div class="inner-wrapper" [@slideSidebarPadding]="{
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'), value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
params: {collapsedSidebarWidth: (collapsedSidebarWidth | async), totalSidebarWidth: (totalSidebarWidth | async)} params: {collapsedSidebarWidth: (collapsedSidebarWidth | async), totalSidebarWidth: (totalSidebarWidth | async)}
}"> }">
<ds-themed-header-navbar-wrapper></ds-themed-header-navbar-wrapper> <ds-themed-header-navbar-wrapper></ds-themed-header-navbar-wrapper>
<ds-notifications-board
[options]="notificationOptions">
</ds-notifications-board>
<main class="main-content"> <main class="main-content">
<ds-themed-breadcrumbs></ds-themed-breadcrumbs> <ds-themed-breadcrumbs></ds-themed-breadcrumbs>
@@ -23,6 +19,9 @@
<ds-themed-footer></ds-themed-footer> <ds-themed-footer></ds-themed-footer>
</div> </div>
</div> </div>
<ds-notifications-board [options]="notificationOptions">
</ds-notifications-board>
<ng-template #fullScreenLoader> <ng-template #fullScreenLoader>
<div class="ds-full-screen-loader"> <div class="ds-full-screen-loader">
<ds-loading [showMessage]="false"></ds-loading> <ds-loading [showMessage]="false"></ds-loading>

View File

@@ -113,3 +113,11 @@ export function dateToString(date: Date | NgbDateStruct): string {
const dateStr = `${year}-${month}-${day}`; const dateStr = `${year}-${month}-${day}`;
return moment.utc(dateStr, 'YYYYMMDD').format('YYYY-MM-DD'); return moment.utc(dateStr, 'YYYYMMDD').format('YYYY-MM-DD');
} }
/**
* Checks if the given string represents a valid date
* @param date the string to be checked
*/
export function isValidDate(date: string) {
return moment(date).isValid();
}

View File

@@ -23,9 +23,7 @@ import { SubmissionFormsConfigService } from '../../../core/config/submission-fo
import { SectionDataObject } from '../models/section-data.model'; import { SectionDataObject } from '../models/section-data.model';
import { SectionsType } from '../sections-type'; import { SectionsType } from '../sections-type';
import { import {
mockSubmissionCollectionId, mockSubmissionCollectionId, mockSubmissionId, mockUploadResponse1ParsedErrors,
mockSubmissionId,
mockUploadResponse1ParsedErrors
} from '../../../shared/mocks/submission.mock'; } from '../../../shared/mocks/submission.mock';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@@ -45,6 +43,7 @@ import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../../core/data/request.service';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { cold } from 'jasmine-marbles'; import { cold } from 'jasmine-marbles';
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
function getMockSubmissionFormsConfigService(): SubmissionFormsConfigService { function getMockSubmissionFormsConfigService(): SubmissionFormsConfigService {
return jasmine.createSpyObj('FormOperationsService', { return jasmine.createSpyObj('FormOperationsService', {
@@ -296,8 +295,10 @@ describe('SubmissionSectionFormComponent test suite', () => {
}; };
compAsAny.formData = {}; compAsAny.formData = {};
compAsAny.sectionMetadata = ['dc.title']; compAsAny.sectionMetadata = ['dc.title'];
spyOn(compAsAny, 'inCurrentSubmissionScope').and.callThrough();
expect(comp.hasMetadataEnrichment(newSectionData)).toBeTruthy(); expect(comp.hasMetadataEnrichment(newSectionData)).toBeTruthy();
expect(compAsAny.inCurrentSubmissionScope).toHaveBeenCalledWith('dc.title');
}); });
it('should return false when has not Metadata Enrichment', () => { it('should return false when has not Metadata Enrichment', () => {
@@ -306,7 +307,10 @@ describe('SubmissionSectionFormComponent test suite', () => {
}; };
compAsAny.formData = newSectionData; compAsAny.formData = newSectionData;
compAsAny.sectionMetadata = ['dc.title']; compAsAny.sectionMetadata = ['dc.title'];
spyOn(compAsAny, 'inCurrentSubmissionScope').and.callThrough();
expect(comp.hasMetadataEnrichment(newSectionData)).toBeFalsy(); expect(comp.hasMetadataEnrichment(newSectionData)).toBeFalsy();
expect(compAsAny.inCurrentSubmissionScope).toHaveBeenCalledWith('dc.title');
}); });
it('should return false when metadata has Metadata Enrichment but not belonging to sectionMetadata', () => { it('should return false when metadata has Metadata Enrichment but not belonging to sectionMetadata', () => {
@@ -318,6 +322,77 @@ describe('SubmissionSectionFormComponent test suite', () => {
expect(comp.hasMetadataEnrichment(newSectionData)).toBeFalsy(); expect(comp.hasMetadataEnrichment(newSectionData)).toBeFalsy();
}); });
describe('inCurrentSubmissionScope', () => {
beforeEach(() => {
// @ts-ignore
comp.formConfig = {
rows: [
{
fields: [
{
selectableMetadata: [{ metadata: 'scoped.workflow' }],
scope: 'WORKFLOW',
} as FormFieldModel
]
},
{
fields: [
{
selectableMetadata: [{ metadata: 'scoped.workspace' }],
scope: 'WORKSPACE',
} as FormFieldModel
]
},
{
fields: [
{
selectableMetadata: [{ metadata: 'dc.title' }],
} as FormFieldModel
]
}
]
};
});
describe('in workspace scope', () => {
beforeEach(() => {
// @ts-ignore
comp.submissionObject = { type: WorkspaceItem.type };
});
it('should return true for unscoped fields', () => {
expect((comp as any).inCurrentSubmissionScope('dc.title')).toBe(true);
});
it('should return true for fields scoped to workspace', () => {
expect((comp as any).inCurrentSubmissionScope('scoped.workspace')).toBe(true);
});
it('should return false for fields scoped to workflow', () => {
expect((comp as any).inCurrentSubmissionScope('scoped.workflow')).toBe(false);
});
});
describe('in workflow scope', () => {
beforeEach(() => {
// @ts-ignore
comp.submissionObject = { type: WorkflowItem.type };
});
it('should return true when field is unscoped', () => {
expect((comp as any).inCurrentSubmissionScope('dc.title')).toBe(true);
});
it('should return true for fields scoped to workflow', () => {
expect((comp as any).inCurrentSubmissionScope('scoped.workflow')).toBe(true);
});
it('should return false for fields scoped to workspace', () => {
expect((comp as any).inCurrentSubmissionScope('scoped.workspace')).toBe(false);
});
});
});
it('should update form properly', () => { it('should update form properly', () => {
spyOn(comp, 'initForm'); spyOn(comp, 'initForm');
spyOn(comp, 'checksForErrors'); spyOn(comp, 'checksForErrors');

View File

@@ -34,6 +34,9 @@ import { followLink } from '../../../shared/utils/follow-link-config.model';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { ConfigObject } from '../../../core/config/models/config.model'; import { ConfigObject } from '../../../core/config/models/config.model';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { SubmissionScopeType } from '../../../core/submission/submission-scope-type';
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
import { SubmissionObject } from '../../../core/submission/models/submission-object.model';
/** /**
* This component represents a section that contains a Form. * This component represents a section that contains a Form.
@@ -112,7 +115,7 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
*/ */
protected subs: Subscription[] = []; protected subs: Subscription[] = [];
protected workspaceItem: WorkspaceItem; protected submissionObject: SubmissionObject;
/** /**
* The FormComponent reference * The FormComponent reference
*/ */
@@ -173,10 +176,10 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
getRemoteDataPayload()) getRemoteDataPayload())
])), ])),
take(1)) take(1))
.subscribe(([sectionData, workspaceItem]: [WorkspaceitemSectionFormObject, WorkspaceItem]) => { .subscribe(([sectionData, submissionObject]: [WorkspaceitemSectionFormObject, SubmissionObject]) => {
if (isUndefined(this.formModel)) { if (isUndefined(this.formModel)) {
// this.sectionData.errorsToShow = []; // this.sectionData.errorsToShow = [];
this.workspaceItem = workspaceItem; this.submissionObject = submissionObject;
// Is the first loading so init form // Is the first loading so init form
this.initForm(sectionData); this.initForm(sectionData);
this.sectionData.data = sectionData; this.sectionData.data = sectionData;
@@ -223,7 +226,7 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
const sectionDataToCheck = {}; const sectionDataToCheck = {};
Object.keys(sectionData).forEach((key) => { Object.keys(sectionData).forEach((key) => {
if (this.sectionMetadata && this.sectionMetadata.includes(key)) { if (this.sectionMetadata && this.sectionMetadata.includes(key) && this.inCurrentSubmissionScope(key)) {
sectionDataToCheck[key] = sectionData[key]; sectionDataToCheck[key] = sectionData[key];
} }
}); });
@@ -246,6 +249,28 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
return isNotEmpty(diffResult); return isNotEmpty(diffResult);
} }
/**
* Whether a specific field is editable in the current scope. Unscoped fields always return true.
* @private
*/
private inCurrentSubmissionScope(field: string): boolean {
const scope = this.formConfig?.rows.find(row => {
return row.fields?.[0]?.selectableMetadata?.[0]?.metadata === field;
}).fields?.[0]?.scope;
switch (scope) {
case SubmissionScopeType.WorkspaceItem: {
return this.submissionObject.type === WorkspaceItem.type;
}
case SubmissionScopeType.WorkflowItem: {
return this.submissionObject.type === WorkflowItem.type;
}
default: {
return true;
}
}
}
/** /**
* Initialize form model * Initialize form model
* *

File diff suppressed because it is too large Load Diff

View File

@@ -1622,6 +1622,19 @@
// "dso-selector.placeholder": "Search for a {{ type }}", // "dso-selector.placeholder": "Search for a {{ type }}",
"dso-selector.placeholder": "Rechercher un(e) {{ type }}", "dso-selector.placeholder": "Rechercher un(e) {{ type }}",
// "dso-selector.select.collection.head": "Select a collection",
"dso-selector.select.collection.head": "Sélectionner une collection",
// "dso-selector.set-scope.community.head": "Select a search scope",
"dso-selector.set-scope.community.head": "Sélectionnez un champ de recherche",
// "dso-selector.set-scope.community.button": "Search all of DSpace",
"dso-selector.set-scope.community.button": "Chercher dans toutes les collections",
// "dso-selector.set-scope.community.input-header": "Search for a community or collection",
"dso-selector.set-scope.community.input-header": "Chercher une communauté ou une collection",
// "confirmation-modal.export-metadata.header": "Export metadata for {{ dsoName }}", // "confirmation-modal.export-metadata.header": "Export metadata for {{ dsoName }}",
"confirmation-modal.export-metadata.header": "Exporter métadonnées de {{ dsoName }}", "confirmation-modal.export-metadata.header": "Exporter métadonnées de {{ dsoName }}",
@@ -4282,11 +4295,31 @@
// "sorting.dc.title.DESC": "Title Descending", // "sorting.dc.title.DESC": "Title Descending",
"sorting.dc.title.DESC": "Titre décroissant", "sorting.dc.title.DESC": "Titre décroissant",
// "sorting.score.DESC": "Relevance", // "sorting.score.ASC": "Least Relevant",
"sorting.score.DESC": "Pertinence", "sorting.score.ASC": "Le moins pertinent",
// "sorting.score.DESC": "Most Relevant",
"sorting.score.DESC": "Le plus pertinent",
// "sorting.dc.date.issued.ASC": "Date Issued Ascending",
"sorting.dc.date.issued.ASC": "Date de publication (croissante)",
// "sorting.dc.date.issued.DESC": "Date Issued Descending",
"sorting.dc.date.issued.DESC": "Date de publication (decroissante)",
// "sorting.dc.date.accessioned.ASC": "Accessioned Date Ascending",
"sorting.dc.date.accessioned.ASC": "Date de dépôt (croissante)",
// "sorting.dc.date.accessioned.DESC": "Accessioned Date Descending",
"sorting.dc.date.accessioned.DESC": "Date de dépôt (decroissante)",
// "sorting.lastModified.ASC": "Last modified Ascending",
"sorting.lastModified.ASC": "Dernière modification (croissante)",
// "sorting.lastModified.DESC": "Last modified Descending",
"sorting.lastModified.ASC": "Dernière modification (descroissante)",
// "statistics.title": "Statistics", // "statistics.title": "Statistics",
"statistics.title": "Statistiques", "statistics.title": "Statistiques",

View File

@@ -3,7 +3,6 @@ import { makeStateKey } from '@angular/platform-browser';
import { Config } from './config.interface'; import { Config } from './config.interface';
import { ServerConfig } from './server-config.interface'; import { ServerConfig } from './server-config.interface';
import { CacheConfig } from './cache-config.interface'; import { CacheConfig } from './cache-config.interface';
import { UniversalConfig } from './universal-config.interface';
import { INotificationBoardOptions } from './notifications-config.interfaces'; import { INotificationBoardOptions } from './notifications-config.interfaces';
import { SubmissionConfig } from './submission-config.interface'; import { SubmissionConfig } from './submission-config.interface';
import { FormConfig } from './form-config.interfaces'; import { FormConfig } from './form-config.interfaces';
@@ -25,7 +24,6 @@ interface AppConfig extends Config {
form: FormConfig; form: FormConfig;
notifications: INotificationBoardOptions; notifications: INotificationBoardOptions;
submission: SubmissionConfig; submission: SubmissionConfig;
universal: UniversalConfig;
debug: boolean; debug: boolean;
defaultLanguage: string; defaultLanguage: string;
languages: LangConfig[]; languages: LangConfig[];

View File

@@ -0,0 +1,6 @@
import { AppConfig } from './app-config.interface';
import { UniversalConfig } from './universal-config.interface';
export interface BuildConfig extends AppConfig {
universal: UniversalConfig;
}

View File

@@ -14,18 +14,10 @@ import { ServerConfig } from './server-config.interface';
import { SubmissionConfig } from './submission-config.interface'; import { SubmissionConfig } from './submission-config.interface';
import { ThemeConfig } from './theme.model'; import { ThemeConfig } from './theme.model';
import { UIServerConfig } from './ui-server-config.interface'; import { UIServerConfig } from './ui-server-config.interface';
import { UniversalConfig } from './universal-config.interface';
export class DefaultAppConfig implements AppConfig { export class DefaultAppConfig implements AppConfig {
production = false; production = false;
// Angular Universal settings
universal: UniversalConfig = {
preboot: true,
async: true,
time: false
};
// NOTE: will log all redux actions and transfers in console // NOTE: will log all redux actions and transfers in console
debug = false; debug = false;

5
src/environments/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
environment.*.ts
!environment.production.ts
!environment.test.ts
!environment.ts

View File

@@ -1,6 +1,6 @@
import { AppConfig } from '../config/app-config.interface'; import { BuildConfig } from '../config/build-config.interface';
export const environment: Partial<AppConfig> = { export const environment: Partial<BuildConfig> = {
production: true, production: true,
// Angular Universal settings // Angular Universal settings

View File

@@ -1,9 +1,9 @@
// This configuration is only used for unit tests, end-to-end tests use environment.production.ts // This configuration is only used for unit tests, end-to-end tests use environment.production.ts
import { BuildConfig } from 'src/config/build-config.interface';
import { RestRequestMethod } from '../app/core/data/rest-request-method'; import { RestRequestMethod } from '../app/core/data/rest-request-method';
import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type'; import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type';
import { AppConfig } from '../config/app-config.interface';
export const environment: AppConfig = { export const environment: BuildConfig = {
production: false, production: false,
// Angular Universal settings // Angular Universal settings

View File

@@ -3,9 +3,9 @@
// `ng test --configuration test` replaces `environment.ts` with `environment.test.ts`. // `ng test --configuration test` replaces `environment.ts` with `environment.test.ts`.
// The list of file replacements can be found in `angular.json`. // The list of file replacements can be found in `angular.json`.
import { AppConfig } from '../config/app-config.interface'; import { BuildConfig } from '../config/build-config.interface';
export const environment: Partial<AppConfig> = { export const environment: Partial<BuildConfig> = {
production: false, production: false,
// Angular Universal settings // Angular Universal settings

View File

@@ -30,7 +30,9 @@ const main = () => {
if (environment.production) { if (environment.production) {
enableProdMode(); enableProdMode();
}
if (hasValue(environment.universal) && environment.universal.preboot) {
return bootstrap(); return bootstrap();
} else { } else {
@@ -47,7 +49,7 @@ const main = () => {
}; };
// support async tag or hmr // support async tag or hmr
if (hasValue(environment.universal) && environment.universal.preboot === false) { if (hasValue(environment.universal) && !environment.universal.preboot) {
main(); main();
} else { } else {
document.addEventListener('DOMContentLoaded', main); document.addEventListener('DOMContentLoaded', main);

30456
yarn.lock

File diff suppressed because it is too large Load Diff