mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge remote-tracking branch '4Science-bitbucket/main' into CST-5337
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
19
README.md
19
README.md
@@ -101,7 +101,7 @@ Installing
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
#### 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
|
||||
To use environment variables in a UI component, use:
|
||||
|
||||
@@ -183,7 +199,6 @@ or
|
||||
import { environment } from '../environment.ts';
|
||||
```
|
||||
|
||||
|
||||
Running the app
|
||||
---------------
|
||||
|
||||
|
186
docker/README.md
186
docker/README.md
@@ -1,93 +1,93 @@
|
||||
# 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.
|
||||
***
|
||||
|
||||
## 'Dockerfile' in root directory
|
||||
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 .
|
||||
```
|
||||
|
||||
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.
|
||||
```
|
||||
docker push dspace/dspace-angular:dspace-7_x
|
||||
```
|
||||
|
||||
## docker directory
|
||||
- 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.
|
||||
- docker-compose-rest.yml
|
||||
- Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes
|
||||
- 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.
|
||||
- cli.yml
|
||||
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
|
||||
- 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.
|
||||
|
||||
|
||||
## To refresh / pull DSpace images from Dockerhub
|
||||
```
|
||||
docker-compose -f docker/docker-compose.yml pull
|
||||
```
|
||||
|
||||
## To build DSpace images using code in your branch
|
||||
```
|
||||
docker-compose -f docker/docker-compose.yml build
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## Run DSpace REST and DSpace Angular from local branches.
|
||||
_The system will be started in 2 steps. Each step shares the same docker network._
|
||||
|
||||
From DSpace/DSpace (build as needed)
|
||||
```
|
||||
docker-compose -p d7 up -d
|
||||
```
|
||||
|
||||
From DSpace/DSpace-angular
|
||||
```
|
||||
docker-compose -p d7 -f docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
## Ingest test data from AIPDIR
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Load content from AIP files
|
||||
```
|
||||
docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
|
||||
```
|
||||
|
||||
## Alternative Ingest - Use Entities dataset
|
||||
_Delete your docker volumes or use a unique project (-p) name_
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## 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._
|
||||
|
||||
```
|
||||
docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d
|
||||
```
|
||||
# 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.
|
||||
***
|
||||
|
||||
## 'Dockerfile' in root directory
|
||||
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 .
|
||||
```
|
||||
|
||||
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.
|
||||
```
|
||||
docker push dspace/dspace-angular:dspace-7_x
|
||||
```
|
||||
|
||||
## docker directory
|
||||
- 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.
|
||||
- docker-compose-rest.yml
|
||||
- Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes
|
||||
- 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.
|
||||
- cli.yml
|
||||
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
|
||||
- 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.
|
||||
|
||||
|
||||
## To refresh / pull DSpace images from Dockerhub
|
||||
```
|
||||
docker-compose -f docker/docker-compose.yml pull
|
||||
```
|
||||
|
||||
## To build DSpace images using code in your branch
|
||||
```
|
||||
docker-compose -f docker/docker-compose.yml build
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## Run DSpace REST and DSpace Angular from local branches.
|
||||
_The system will be started in 2 steps. Each step shares the same docker network._
|
||||
|
||||
From DSpace/DSpace (build as needed)
|
||||
```
|
||||
docker-compose -p d7 up -d
|
||||
```
|
||||
|
||||
From DSpace/DSpace-angular
|
||||
```
|
||||
docker-compose -p d7 -f docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
## Ingest test data from AIPDIR
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Load content from AIP files
|
||||
```
|
||||
docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
|
||||
```
|
||||
|
||||
## Alternative Ingest - Use Entities dataset
|
||||
_Delete your docker volumes or use a unique project (-p) name_
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## 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._
|
||||
|
||||
```
|
||||
docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d
|
||||
```
|
||||
|
@@ -116,7 +116,7 @@
|
||||
"rxjs": "^6.6.3",
|
||||
"sortablejs": "1.13.0",
|
||||
"tslib": "^2.0.0",
|
||||
"url-parse": "^1.5.3",
|
||||
"url-parse": "^1.5.6",
|
||||
"uuid": "^8.3.2",
|
||||
"webfontloader": "1.6.28",
|
||||
"zone.js": "^0.10.3"
|
||||
@@ -158,7 +158,7 @@
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-marbles": "0.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "^5.2.3",
|
||||
"karma": "^6.3.14",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
|
@@ -63,13 +63,14 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||
|
||||
inFocus$: BehaviorSubject<boolean>;
|
||||
|
||||
constructor(protected menuService: MenuService,
|
||||
constructor(
|
||||
protected menuService: MenuService,
|
||||
protected injector: Injector,
|
||||
private variableService: CSSVariableService,
|
||||
private authService: AuthService,
|
||||
private modalService: NgbModal,
|
||||
protected variableService: CSSVariableService,
|
||||
protected authService: AuthService,
|
||||
protected modalService: NgbModal,
|
||||
public authorizationService: AuthorizationDataService,
|
||||
private scriptDataService: ScriptDataService,
|
||||
protected scriptDataService: ScriptDataService,
|
||||
public route: ActivatedRoute
|
||||
) {
|
||||
super(menuService, injector, authorizationService, route);
|
||||
|
@@ -0,0 +1,25 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { AdminSidebarComponent } from './admin-sidebar.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for AdminSidebarComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-admin-sidebar',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedAdminSidebarComponent extends ThemedComponent<AdminSidebarComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'AdminSidebarComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/admin/admin-sidebar/admin-sidebar.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./admin-sidebar.component');
|
||||
}
|
||||
}
|
@@ -56,6 +56,7 @@ import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-
|
||||
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
|
||||
import { PageInternalServerErrorComponent } from './page-internal-server-error/page-internal-server-error.component';
|
||||
import { ThemedAdminSidebarComponent } from './admin/admin-sidebar/themed-admin-sidebar.component';
|
||||
|
||||
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
|
||||
|
||||
@@ -171,6 +172,7 @@ const DECLARATIONS = [
|
||||
HeaderNavbarWrapperComponent,
|
||||
ThemedHeaderNavbarWrapperComponent,
|
||||
AdminSidebarComponent,
|
||||
ThemedAdminSidebarComponent,
|
||||
AdminSidebarSectionComponent,
|
||||
ExpandableAdminSidebarSectionComponent,
|
||||
FooterComponent,
|
||||
|
@@ -18,6 +18,7 @@ import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { isValidDate } from '../../shared/date.util';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-date-page',
|
||||
@@ -85,10 +86,10 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||
let lowerLimit = environment.browseBy.defaultLowerLimit;
|
||||
if (hasValue(firstItemRD.payload)) {
|
||||
const date = firstItemRD.payload.firstMetadataValue(metadataKeys);
|
||||
if (hasValue(date)) {
|
||||
if (isNotEmpty(date) && isValidDate(date)) {
|
||||
const dateObj = new Date(date);
|
||||
// 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 = [];
|
||||
|
@@ -18,7 +18,7 @@
|
||||
</ds-comcol-page-content>
|
||||
</header>
|
||||
<!-- Browse-By Links -->
|
||||
<ds-comcol-page-browse-by [id]="parentContext.id" [contentType]="parentContext.type"></ds-comcol-page-browse-by>
|
||||
<ds-themed-comcol-page-browse-by [id]="parentContext.id" [contentType]="parentContext.type"></ds-themed-comcol-page-browse-by>
|
||||
</ng-container></ng-container>
|
||||
|
||||
<section class="comcol-page-browse-section">
|
||||
|
@@ -40,10 +40,10 @@
|
||||
</div>
|
||||
<section class="comcol-page-browse-section">
|
||||
<!-- Browse-By Links -->
|
||||
<ds-comcol-page-browse-by
|
||||
<ds-themed-comcol-page-browse-by
|
||||
[id]="collection.id"
|
||||
[contentType]="collection.type">
|
||||
</ds-comcol-page-browse-by>
|
||||
</ds-themed-comcol-page-browse-by>
|
||||
|
||||
<ng-container *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div class="mt-4" *ngIf="itemRD?.hasSucceeded" @fadeIn>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div class="container">
|
||||
<h2>{{ 'communityList.title' | translate }}</h2>
|
||||
<ds-community-list></ds-community-list>
|
||||
<ds-themed-community-list></ds-themed-community-list>
|
||||
</div>
|
||||
|
@@ -5,12 +5,14 @@ import { CommunityListPageComponent } from './community-list-page.component';
|
||||
import { CommunityListPageRoutingModule } from './community-list-page.routing.module';
|
||||
import { CommunityListComponent } from './community-list/community-list.component';
|
||||
import { ThemedCommunityListPageComponent } from './themed-community-list-page.component';
|
||||
import { ThemedCommunityListComponent } from './community-list/themed-community-list.component';
|
||||
|
||||
|
||||
const DECLARATIONS = [
|
||||
CommunityListPageComponent,
|
||||
CommunityListComponent,
|
||||
ThemedCommunityListPageComponent
|
||||
ThemedCommunityListPageComponent,
|
||||
ThemedCommunityListComponent
|
||||
];
|
||||
/**
|
||||
* The page which houses a title and the community list, as described in community-list.component
|
||||
|
@@ -0,0 +1,23 @@
|
||||
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||
import { CommunityListComponent } from './community-list.component';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'ds-themed-community-list',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||
})export class ThemedCommunityListComponent extends ThemedComponent<CommunityListComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'CommunityListComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/community-list-page/community-list/community-list.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import(`./community-list.component`);
|
||||
}
|
||||
|
||||
}
|
@@ -26,8 +26,8 @@
|
||||
</div>
|
||||
<section class="comcol-page-browse-section">
|
||||
<!-- Browse-By Links -->
|
||||
<ds-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type">
|
||||
</ds-comcol-page-browse-by>
|
||||
<ds-themed-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type">
|
||||
</ds-themed-comcol-page-browse-by>
|
||||
|
||||
<ds-community-page-sub-community-list [community]="communityPayload"></ds-community-page-sub-community-list>
|
||||
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>
|
||||
|
@@ -219,6 +219,9 @@ describe('AuthEffects', () => {
|
||||
const expected = cold('--b-', { b: new RetrieveTokenAction() });
|
||||
|
||||
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', () => {
|
||||
|
@@ -162,6 +162,7 @@ export class AuthEffects {
|
||||
return this.authService.checkAuthenticationCookie().pipe(
|
||||
map((response: AuthStatus) => {
|
||||
if (response.authenticated) {
|
||||
this.authorizationsService.invalidateAuthorizationsRequestCache();
|
||||
return new RetrieveTokenAction();
|
||||
} else {
|
||||
return new RetrieveAuthMethodsAction(response);
|
||||
|
@@ -3,7 +3,7 @@ import { compare } from 'fast-json-patch';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { getClassForType } from '../cache/builders/build-decorators';
|
||||
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';
|
||||
|
||||
/**
|
||||
@@ -22,8 +22,8 @@ export class DefaultChangeAnalyzer<T extends TypedObject> implements ChangeAnaly
|
||||
* The second object to compare
|
||||
*/
|
||||
diff(object1: T, object2: T): Operation[] {
|
||||
const serializer1 = new DSpaceSerializer(getClassForType(object1.type));
|
||||
const serializer2 = new DSpaceSerializer(getClassForType(object2.type));
|
||||
const serializer1 = new DSpaceNotNullSerializer(getClassForType(object1.type));
|
||||
const serializer2 = new DSpaceNotNullSerializer(getClassForType(object2.type));
|
||||
return compare(serializer1.serialize(object1), serializer2.serialize(object2));
|
||||
}
|
||||
}
|
||||
|
@@ -90,10 +90,12 @@ describe('EpersonRegistrationService', () => {
|
||||
const expected = service.searchByToken('test-token');
|
||||
|
||||
expect(expected).toBeObservable(cold('(a|)', {
|
||||
a: Object.assign(new Registration(), {
|
||||
email: registrationWithUser.email,
|
||||
token: 'test-token',
|
||||
user: registrationWithUser.user
|
||||
a: jasmine.objectContaining({
|
||||
payload: Object.assign(new Registration(), {
|
||||
email: registrationWithUser.email,
|
||||
token: 'test-token',
|
||||
user: registrationWithUser.user
|
||||
})
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
@@ -79,7 +79,7 @@ export class EpersonRegistrationService {
|
||||
* Search a registration based on the provided token
|
||||
* @param token
|
||||
*/
|
||||
searchByToken(token: string): Observable<Registration> {
|
||||
searchByToken(token: string): Observable<RemoteData<Registration>> {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
const href$ = this.getTokenSearchEndpoint(token).pipe(
|
||||
@@ -97,15 +97,14 @@ export class EpersonRegistrationService {
|
||||
});
|
||||
|
||||
return this.rdbService.buildSingle<Registration>(href$).pipe(
|
||||
skipWhile((rd: RemoteData<Registration>) => rd.isStale),
|
||||
getFirstSucceededRemoteData(),
|
||||
map((restResponse: RemoteData<Registration>) => {
|
||||
return Object.assign(new Registration(), {
|
||||
email: restResponse.payload.email, token: token, user: restResponse.payload.user
|
||||
});
|
||||
}),
|
||||
map((rd) => {
|
||||
if (rd.hasSucceeded && hasValue(rd.payload)) {
|
||||
return Object.assign(rd, { payload: Object.assign(rd.payload, { token }) });
|
||||
} else {
|
||||
return rd;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
76
src/app/core/dspace-rest/dspace-not-null.serializer.ts
Normal file
76
src/app/core/dspace-rest/dspace-not-null.serializer.ts
Normal 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;
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
<div class="container">
|
||||
<div class="container" *ngIf="(registration$ |async)">
|
||||
<h3 class="mb-4">{{'forgot-password.form.head' | translate}}</h3>
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">{{'forgot-password.form.identification.header' | translate}}</div>
|
||||
@@ -33,4 +33,4 @@
|
||||
(click)="submit()">{{'forgot-password.form.submit' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -16,7 +16,11 @@ import { Registration } from '../../core/shared/registration.model';
|
||||
import { ForgotPasswordFormComponent } from './forgot-password-form.component';
|
||||
import { By } from '@angular/platform-browser';
|
||||
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', () => {
|
||||
let comp: ForgotPasswordFormComponent;
|
||||
@@ -36,7 +40,7 @@ describe('ForgotPasswordFormComponent', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
|
||||
route = {data: observableOf({registration: registration})};
|
||||
route = {data: observableOf({registration: createSuccessfulRemoteDataObject(registration)})};
|
||||
router = new RouterStub();
|
||||
notificationsService = new NotificationsServiceStub();
|
||||
|
||||
|
@@ -11,7 +11,10 @@ import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../../core/core.reducers';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||
import {
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
} from '../../core/shared/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-forgot-password-form',
|
||||
@@ -48,7 +51,8 @@ export class ForgotPasswordFormComponent {
|
||||
|
||||
ngOnInit(): void {
|
||||
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.email = registration.email;
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
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 { ThemedForgotEmailComponent } from './forgot-password-email/themed-forgot-email.component';
|
||||
import { RegistrationGuard } from '../register-page/registration.guard';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -16,12 +16,11 @@ import { ThemedForgotEmailComponent } from './forgot-password-email/themed-forgo
|
||||
{
|
||||
path: ':token',
|
||||
component: ThemedForgotPasswordFormComponent,
|
||||
resolve: {registration: RegistrationResolver}
|
||||
canActivate: [ RegistrationGuard ],
|
||||
}
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
RegistrationResolver,
|
||||
ItemPageResolver,
|
||||
]
|
||||
})
|
||||
|
@@ -2,9 +2,9 @@
|
||||
<ds-my-dspace-new-submission *dsShowOnlyForRole="[roleTypeEnum.Submitter]"></ds-my-dspace-new-submission>
|
||||
</div>
|
||||
|
||||
<ds-search *ngIf="configuration && context"
|
||||
<ds-themed-search *ngIf="configuration && context"
|
||||
[configuration]="configuration"
|
||||
[configurationList]="(configurationList$ | async)"
|
||||
[context]="context"
|
||||
[viewModeList]="viewModeList"
|
||||
></ds-search>
|
||||
></ds-themed-search>
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<div id="collapsingNav">
|
||||
<ul class="navbar-nav mr-auto shadow-none">
|
||||
<ng-container *ngFor="let section of (sections | async)">
|
||||
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
<ng-container *ngComponentOutlet="(sectionMap$ | async).get(section.id)?.component; injector: (sectionMap$ | async).get(section.id)?.injector;"></ng-container>
|
||||
</ng-container>
|
||||
</ul>
|
||||
</div>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { RegistrationResolver } from './registration.resolver';
|
||||
import { EpersonRegistrationService } from '../core/data/eperson-registration.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { Registration } from '../core/shared/registration.model';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||
|
||||
describe('RegistrationResolver', () => {
|
||||
let resolver: RegistrationResolver;
|
||||
@@ -13,7 +13,7 @@ describe('RegistrationResolver', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
|
||||
searchByToken: observableOf(registration)
|
||||
searchByToken: createSuccessfulRemoteDataObject$(registration)
|
||||
});
|
||||
resolver = new RegistrationResolver(epersonRegistrationService);
|
||||
});
|
||||
@@ -23,9 +23,9 @@ describe('RegistrationResolver', () => {
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
(resolved) => {
|
||||
expect(resolved.token).toEqual(token);
|
||||
expect(resolved.email).toEqual('test@email.org');
|
||||
expect(resolved.user).toEqual('user-uuid');
|
||||
expect(resolved.payload.token).toEqual(token);
|
||||
expect(resolved.payload.email).toEqual('test@email.org');
|
||||
expect(resolved.payload.user).toEqual('user-uuid');
|
||||
done();
|
||||
}
|
||||
);
|
||||
|
@@ -3,18 +3,22 @@ import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/r
|
||||
import { EpersonRegistrationService } from '../core/data/eperson-registration.service';
|
||||
import { Registration } from '../core/shared/registration.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||
|
||||
@Injectable()
|
||||
/**
|
||||
* 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) {
|
||||
}
|
||||
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Registration> {
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Registration>> {
|
||||
const token = route.params.token;
|
||||
return this.epersonRegistrationService.searchByToken(token);
|
||||
return this.epersonRegistrationService.searchByToken(token).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div class="container">
|
||||
<div class="container" *ngIf="(registration$ |async)">
|
||||
<h3 class="mb-4">{{'register-page.create-profile.header' | translate}}</h3>
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">{{'register-page.create-profile.identification.header' | translate}}</div>
|
||||
|
@@ -21,7 +21,11 @@ import {
|
||||
END_USER_AGREEMENT_METADATA_FIELD,
|
||||
EndUserAgreementService
|
||||
} 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', () => {
|
||||
let comp: CreateProfileComponent;
|
||||
@@ -106,7 +110,7 @@ describe('CreateProfileComponent', () => {
|
||||
};
|
||||
epersonWithAgreement = Object.assign(new EPerson(), valuesWithAgreement);
|
||||
|
||||
route = {data: observableOf({registration: registration})};
|
||||
route = {data: observableOf({registration: createSuccessfulRemoteDataObject(registration)})};
|
||||
router = new RouterStub();
|
||||
notificationsService = new NotificationsServiceStub();
|
||||
|
||||
|
@@ -19,7 +19,7 @@ import {
|
||||
END_USER_AGREEMENT_METADATA_FIELD,
|
||||
EndUserAgreementService
|
||||
} 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
|
||||
@@ -56,7 +56,8 @@ export class CreateProfileComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
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.email = registration.email;
|
||||
|
@@ -2,9 +2,9 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { RegisterEmailComponent } from './register-email/register-email.component';
|
||||
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 { ThemedCreateProfileComponent } from './create-profile/themed-create-profile.component';
|
||||
import { RegistrationGuard } from './registration.guard';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -17,13 +17,14 @@ import { ThemedCreateProfileComponent } from './create-profile/themed-create-pro
|
||||
{
|
||||
path: ':token',
|
||||
component: ThemedCreateProfileComponent,
|
||||
resolve: {registration: RegistrationResolver},
|
||||
canActivate: [EndUserAgreementCookieGuard]
|
||||
canActivate: [
|
||||
RegistrationGuard,
|
||||
EndUserAgreementCookieGuard,
|
||||
],
|
||||
}
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
RegistrationResolver,
|
||||
ItemPageResolver
|
||||
]
|
||||
})
|
||||
|
106
src/app/register-page/registration.guard.spec.ts
Normal file
106
src/app/register-page/registration.guard.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
43
src/app/register-page/registration.guard.ts
Normal file
43
src/app/register-page/registration.guard.ts
Normal 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;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@@ -1,14 +1,10 @@
|
||||
<div class="outer-wrapper" *ngIf="!shouldShowFullscreenLoader; else fullScreenLoader">
|
||||
<ds-admin-sidebar></ds-admin-sidebar>
|
||||
<div class="inner-wrapper" [@slideSidebarPadding]="{
|
||||
<ds-themed-admin-sidebar></ds-themed-admin-sidebar>
|
||||
<div class="inner-wrapper" [@slideSidebarPadding]="{
|
||||
value: (!(sidebarVisible | async) ? 'hidden' : (slideSidebarOver | async) ? 'shown' : 'expanded'),
|
||||
params: {collapsedSidebarWidth: (collapsedSidebarWidth | async), totalSidebarWidth: (totalSidebarWidth | async)}
|
||||
}">
|
||||
<ds-themed-header-navbar-wrapper></ds-themed-header-navbar-wrapper>
|
||||
|
||||
<ds-notifications-board
|
||||
[options]="notificationOptions">
|
||||
</ds-notifications-board>
|
||||
<main class="main-content">
|
||||
<ds-themed-breadcrumbs></ds-themed-breadcrumbs>
|
||||
|
||||
@@ -23,6 +19,9 @@
|
||||
<ds-themed-footer></ds-themed-footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ds-notifications-board [options]="notificationOptions">
|
||||
</ds-notifications-board>
|
||||
<ng-template #fullScreenLoader>
|
||||
<div class="ds-full-screen-loader">
|
||||
<ds-loading [showMessage]="false"></ds-loading>
|
||||
|
@@ -27,7 +27,6 @@ import { Router } from '@angular/router';
|
||||
})
|
||||
|
||||
export class ConfigurationSearchPageComponent extends SearchComponent {
|
||||
|
||||
constructor(protected service: SearchService,
|
||||
protected sidebarService: SidebarService,
|
||||
protected windowService: HostWindowService,
|
||||
@@ -36,5 +35,4 @@ export class ConfigurationSearchPageComponent extends SearchComponent {
|
||||
protected router: Router) {
|
||||
super(service, sidebarService, windowService, searchConfigService, routeService, router);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,2 +1,2 @@
|
||||
<ds-search></ds-search>
|
||||
<ds-themed-search></ds-themed-search>
|
||||
<ds-search-tracker></ds-search-tracker>
|
||||
|
@@ -1,3 +1,8 @@
|
||||
<div class="container">
|
||||
<h3>{{'bitstream.download.page' | translate:{bitstream: (bitstream$ | async)?.name} }}</h3>
|
||||
<h3>{{'bitstream.download.page' | translate:{bitstream: (bitstream$ | async)?.name} }}</h3>
|
||||
<div class="pt-3">
|
||||
<button (click)="back()" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> {{'bitstream.download.page.back' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -12,6 +12,7 @@ import { FileService } from '../../core/shared/file.service';
|
||||
import { HardRedirectService } from '../../core/services/hard-redirect.service';
|
||||
import { getForbiddenRoute } from '../../app-routing-paths';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Location } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-bitstream-download-page',
|
||||
@@ -33,10 +34,15 @@ export class BitstreamDownloadPageComponent implements OnInit {
|
||||
private auth: AuthService,
|
||||
private fileService: FileService,
|
||||
private hardRedirectService: HardRedirectService,
|
||||
private location: Location,
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
back(): void {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.bitstreamRD$ = this.route.data.pipe(
|
||||
|
@@ -0,0 +1,33 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ComcolPageBrowseByComponent } from './comcol-page-browse-by.component';
|
||||
import { ThemedComponent } from '../../theme-support/themed.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for ComcolPageBrowseByComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-comcol-page-browse-by',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedComcolPageBrowseByComponent extends ThemedComponent<ComcolPageBrowseByComponent> {
|
||||
/**
|
||||
* The ID of the Community or Collection
|
||||
*/
|
||||
@Input() id: string;
|
||||
@Input() contentType: string;
|
||||
|
||||
inAndOutputNames: (keyof ComcolPageBrowseByComponent & keyof this)[] = ['id', 'contentType'];
|
||||
|
||||
protected getComponentName(): string {
|
||||
return 'ComcolPageBrowseByComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../../themes/${themeName}/app/shared/comcol-page-browse-by/comcol-page-browse-by.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./comcol-page-browse-by.component');
|
||||
}
|
||||
}
|
@@ -9,6 +9,7 @@ import { CreateComColPageComponent } from './comcol-forms/create-comcol-page/cre
|
||||
import { EditComColPageComponent } from './comcol-forms/edit-comcol-page/edit-comcol-page.component';
|
||||
import { DeleteComColPageComponent } from './comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||
import { ComcolPageBrowseByComponent } from './comcol-page-browse-by/comcol-page-browse-by.component';
|
||||
import { ThemedComcolPageBrowseByComponent } from './comcol-page-browse-by/themed-comcol-page-browse-by.component';
|
||||
import { ComcolRoleComponent } from './comcol-forms/edit-comcol-page/comcol-role/comcol-role.component';
|
||||
import { SharedModule } from '../shared.module';
|
||||
import { FormModule } from '../form/form.module';
|
||||
@@ -23,6 +24,7 @@ const COMPONENTS = [
|
||||
EditComColPageComponent,
|
||||
DeleteComColPageComponent,
|
||||
ComcolPageBrowseByComponent,
|
||||
ThemedComcolPageBrowseByComponent,
|
||||
ComcolRoleComponent,
|
||||
];
|
||||
|
||||
|
@@ -113,3 +113,11 @@ export function dateToString(date: Date | NgbDateStruct): string {
|
||||
const dateStr = `${year}-${month}-${day}`;
|
||||
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();
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ import { NotificationsService } from '../../../notifications/notifications.servi
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { FindListOptions } from '../../../../core/data/request.models';
|
||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-authorized-collection-selector',
|
||||
@@ -31,11 +32,14 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
|
||||
*/
|
||||
@Input() entityType: string;
|
||||
|
||||
constructor(protected searchService: SearchService,
|
||||
protected collectionDataService: CollectionDataService,
|
||||
protected notifcationsService: NotificationsService,
|
||||
protected translate: TranslateService) {
|
||||
super(searchService, notifcationsService, translate);
|
||||
constructor(
|
||||
protected searchService: SearchService,
|
||||
protected collectionDataService: CollectionDataService,
|
||||
protected notifcationsService: NotificationsService,
|
||||
protected translate: TranslateService,
|
||||
protected dsoNameService: DSONameService,
|
||||
) {
|
||||
super(searchService, notifcationsService, translate, dsoNameService);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -22,7 +22,7 @@
|
||||
<button *ngFor="let listEntry of (listEntries$ | async)"
|
||||
class="list-group-item list-group-item-action border-0 list-entry"
|
||||
[ngClass]="{'bg-primary': listEntry.indexableObject.id === currentDSOId}"
|
||||
title="{{ listEntry.indexableObject.name }}"
|
||||
title="{{ getName(listEntry) }}"
|
||||
dsHoverClass="ds-hover"
|
||||
(click)="onSelect.emit(listEntry.indexableObject)" #listEntryElement>
|
||||
<ds-listable-object-component-loader [object]="listEntry" [viewMode]="viewMode"
|
||||
|
@@ -34,6 +34,7 @@ import { SearchResult } from '../../search/models/search-result.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { NotificationsService } from '../../notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dso-selector',
|
||||
@@ -126,9 +127,12 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
public subs: Subscription[] = [];
|
||||
|
||||
constructor(protected searchService: SearchService,
|
||||
protected notifcationsService: NotificationsService,
|
||||
protected translate: TranslateService) {
|
||||
constructor(
|
||||
protected searchService: SearchService,
|
||||
protected notifcationsService: NotificationsService,
|
||||
protected translate: TranslateService,
|
||||
protected dsoNameService: DSONameService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,4 +261,8 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
|
||||
ngOnDestroy(): void {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
getName(searchResult: SearchResult<DSpaceObject>): string {
|
||||
return this.dsoNameService.getName(searchResult.indexableObject);
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ds-search *ngIf="this.relationship.searchConfiguration && context"
|
||||
<ds-themed-search *ngIf="this.relationship.searchConfiguration && context"
|
||||
[configuration]="this.relationship.searchConfiguration"
|
||||
[context]="context"
|
||||
[fixedFilterQuery]="this.relationship.filter"
|
||||
@@ -60,4 +60,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ds-search>
|
||||
</ds-themed-search>
|
||||
|
@@ -1,18 +1,19 @@
|
||||
:host ::ng-deep {
|
||||
--ds-wrapper-grid-spacing: calc(var(--bs-spacer) / 2);
|
||||
|
||||
div.thumbnail > .thumbnail-content {
|
||||
height: var(--ds-card-thumbnail-height);
|
||||
width: 100%;
|
||||
display: block;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: 50% 15%;
|
||||
}
|
||||
div.card {
|
||||
margin-top: var(--ds-wrapper-grid-spacing);
|
||||
margin-bottom: var(--ds-wrapper-grid-spacing);
|
||||
|
||||
div.thumbnail > .thumbnail-content {
|
||||
height: var(--ds-card-thumbnail-height);
|
||||
width: 100%;
|
||||
display: block;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: 50% 15%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,4 +27,3 @@
|
||||
padding-right: var(--ds-wrapper-grid-spacing);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
|
||||
<div class="alert alert-success w-100" role="alert">
|
||||
<h4 class="alert-heading">Approved</h4>
|
||||
<ds-item-list-preview *ngIf="workflowitem"
|
||||
<ds-themed-item-list-preview *ngIf="workflowitem"
|
||||
[item]="(workflowitem?.item | async)?.payload"
|
||||
[object]="object"
|
||||
[status]="status"
|
||||
[showSubmitter]="showSubmitter"></ds-item-list-preview>
|
||||
[showSubmitter]="showSubmitter"></ds-themed-item-list-preview>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
|
||||
<div class="alert alert-secondary w-100" role="alert">
|
||||
<h4 class="alert-heading">Declined</h4>
|
||||
<ds-item-list-preview *ngIf="workflowitem"
|
||||
<ds-themed-item-list-preview *ngIf="workflowitem"
|
||||
[item]="(workflowitem?.item | async)?.payload"
|
||||
[object]="object"
|
||||
[status]="status"
|
||||
[showSubmitter]="showSubmitter"></ds-item-list-preview>
|
||||
[showSubmitter]="showSubmitter"></ds-themed-item-list-preview>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@@ -1,9 +1,9 @@
|
||||
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
|
||||
<ds-item-list-preview *ngIf="workflowitem"
|
||||
<ds-themed-item-list-preview *ngIf="workflowitem"
|
||||
[item]="(workflowitem?.item | async)?.payload"
|
||||
[object]="object"
|
||||
[showSubmitter]="showSubmitter"
|
||||
[status]="status"></ds-item-list-preview>
|
||||
[status]="status"></ds-themed-item-list-preview>
|
||||
<ds-claimed-task-actions *ngIf="workflowitem" [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-claimed-task-actions>
|
||||
</ng-container>
|
||||
|
||||
|
@@ -0,0 +1,38 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ThemedComponent } from '../../../theme-support/themed.component';
|
||||
import { ItemListPreviewComponent } from './item-list-preview.component';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||
import { SearchResult } from '../../../search/models/search-result.model';
|
||||
|
||||
/**
|
||||
* Themed wrapper for ItemListPreviewComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-item-list-preview',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../../theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedItemListPreviewComponent extends ThemedComponent<ItemListPreviewComponent> {
|
||||
protected inAndOutputNames: (keyof ItemListPreviewComponent & keyof this)[] = ['item', 'object', 'status', 'showSubmitter'];
|
||||
|
||||
@Input() item: Item;
|
||||
|
||||
@Input() object: SearchResult<any>;
|
||||
|
||||
@Input() status: MyDspaceItemStatusType;
|
||||
|
||||
@Input() showSubmitter = false;
|
||||
|
||||
protected getComponentName(): string {
|
||||
return 'ItemListPreviewComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../../../themes/${themeName}/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./item-list-preview.component');
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
<ds-item-list-preview [item]="dso"
|
||||
<ds-themed-item-list-preview [item]="dso"
|
||||
[object]="object"
|
||||
[status]="status"></ds-item-list-preview>
|
||||
[status]="status"></ds-themed-item-list-preview>
|
||||
|
||||
<ds-item-actions [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-item-actions>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
|
||||
<ds-item-list-preview *ngIf="workflowitem"
|
||||
<ds-themed-item-list-preview *ngIf="workflowitem"
|
||||
[item]="(workflowitem?.item | async)?.payload"
|
||||
[object]="object"
|
||||
[showSubmitter]="showSubmitter"
|
||||
[status]="status"></ds-item-list-preview>
|
||||
[status]="status"></ds-themed-item-list-preview>
|
||||
<ds-pool-task-actions id="actions" *ngIf="workflowitem" [object]="dso" (processCompleted)="this.reloadedObject.emit($event.reloadedObject)"></ds-pool-task-actions>
|
||||
</ng-container>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<ng-container *ngIf="item$ | async">
|
||||
<ds-item-list-preview
|
||||
<ds-themed-item-list-preview
|
||||
[item]="item$ | async"
|
||||
[object]="object"
|
||||
[status]="status"></ds-item-list-preview>
|
||||
[status]="status"></ds-themed-item-list-preview>
|
||||
|
||||
<ds-workflowitem-actions [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-workflowitem-actions>
|
||||
</ng-container>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<ng-container *ngIf="item$ | async">
|
||||
<ds-item-list-preview
|
||||
<ds-themed-item-list-preview
|
||||
[item]="item$ | async"
|
||||
[object]="object"
|
||||
[status]="status"></ds-item-list-preview>
|
||||
[status]="status"></ds-themed-item-list-preview>
|
||||
|
||||
<ds-workspaceitem-actions [object]="dso" (processCompleted)="reloadedObject.emit($event.reloadedObject)"></ds-workspaceitem-actions>
|
||||
</ng-container>
|
||||
|
@@ -47,11 +47,23 @@ describe('SearchFormComponent', () => {
|
||||
el = de.nativeElement;
|
||||
});
|
||||
|
||||
it('should not display scopes when empty', () => {
|
||||
it('should not display scopes when showScopeSelector is false', fakeAsync(() => {
|
||||
comp.showScopeSelector = false;
|
||||
|
||||
fixture.detectChanges();
|
||||
const select = de.query(By.css('select'));
|
||||
expect(select).toBeNull();
|
||||
});
|
||||
tick();
|
||||
|
||||
expect(de.query(By.css('.scope-button'))).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should display scopes when showScopeSelector is true', fakeAsync(() => {
|
||||
comp.showScopeSelector = true;
|
||||
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(de.query(By.css('.scope-button'))).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should display set query value in input field', fakeAsync(() => {
|
||||
const testString = 'This is a test query';
|
||||
|
@@ -78,7 +78,7 @@
|
||||
[query]="(searchOptions$ | async)?.query"
|
||||
[scope]="(searchOptions$ | async)?.scope"
|
||||
[currentUrl]="searchLink"
|
||||
[showScopeSelector]="true"
|
||||
[showScopeSelector]="showScopeSelector"
|
||||
[inPlaceSearch]="inPlaceSearch"
|
||||
[searchPlaceholder]="searchFormPlaceholder | translate">
|
||||
</ds-search-form>
|
||||
|
@@ -134,6 +134,11 @@ export class SearchComponent implements OnInit {
|
||||
*/
|
||||
@Input() viewModeList: ViewMode[];
|
||||
|
||||
/**
|
||||
* Defines whether or not to show the scope selector
|
||||
*/
|
||||
@Input() showScopeSelector = false;
|
||||
|
||||
/**
|
||||
* The current configuration used during the search
|
||||
*/
|
||||
|
@@ -28,9 +28,11 @@ import { MissingTranslationHelper } from '../translate/missing-translation.helpe
|
||||
import { SharedModule } from '../shared.module';
|
||||
import { SearchResultsComponent } from './search-results/search-results.component';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { ThemedSearchComponent } from './themed-search.component';
|
||||
|
||||
const COMPONENTS = [
|
||||
SearchComponent,
|
||||
ThemedSearchComponent,
|
||||
SearchResultsComponent,
|
||||
SearchSidebarComponent,
|
||||
SearchSettingsComponent,
|
||||
|
75
src/app/shared/search/themed-search.component.ts
Normal file
75
src/app/shared/search/themed-search.component.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { ThemedComponent } from '../theme-support/themed.component';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { SearchConfigurationOption } from './search-switch-configuration/search-configuration-option.model';
|
||||
import { Context } from '../../core/shared/context.model';
|
||||
import { CollectionElementLinkType } from '../object-collection/collection-element-link.type';
|
||||
import { SelectionConfig } from './search-results/search-results.component';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
import { SearchObjects } from './models/search-objects.model';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
||||
|
||||
/**
|
||||
* Themed wrapper for SearchComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-search',
|
||||
styleUrls: [],
|
||||
templateUrl: '../theme-support/themed.component.html',
|
||||
})
|
||||
export class ThemedSearchComponent extends ThemedComponent<SearchComponent> {
|
||||
protected inAndOutputNames: (keyof SearchComponent & keyof this)[] = ['configurationList', 'context', 'configuration', 'fixedFilterQuery', 'useCachedVersionIfAvailable', 'inPlaceSearch', 'linkType', 'paginationId', 'searchEnabled', 'sideBarWidth', 'searchFormPlaceholder', 'selectable', 'selectionConfig', 'showSidebar', 'showViewModes', 'useUniquePageId', 'viewModeList', 'resultFound', 'deselectObject', 'selectObject'];
|
||||
|
||||
@Input() configurationList: SearchConfigurationOption[] = [];
|
||||
|
||||
@Input() context: Context = Context.Search;
|
||||
|
||||
@Input() configuration = 'default';
|
||||
|
||||
@Input() fixedFilterQuery: string;
|
||||
|
||||
@Input() useCachedVersionIfAvailable = true;
|
||||
|
||||
@Input() inPlaceSearch = true;
|
||||
|
||||
@Input() linkType: CollectionElementLinkType;
|
||||
|
||||
@Input() paginationId = 'spc';
|
||||
|
||||
@Input() searchEnabled = true;
|
||||
|
||||
@Input() sideBarWidth = 3;
|
||||
|
||||
@Input() searchFormPlaceholder = 'search.search-form.placeholder';
|
||||
|
||||
@Input() selectable = false;
|
||||
|
||||
@Input() selectionConfig: SelectionConfig;
|
||||
|
||||
@Input() showSidebar = true;
|
||||
|
||||
@Input() showViewModes = true;
|
||||
|
||||
@Input() useUniquePageId: false;
|
||||
|
||||
@Input() viewModeList: ViewMode[];
|
||||
|
||||
@Output() resultFound: EventEmitter<SearchObjects<DSpaceObject>> = new EventEmitter<SearchObjects<DSpaceObject>>();
|
||||
|
||||
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||
|
||||
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
||||
|
||||
protected getComponentName(): string {
|
||||
return 'SearchComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../themes/${themeName}/app/shared/search/search.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./search.component');
|
||||
}
|
||||
}
|
@@ -7,13 +7,8 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
|
||||
import { NouisliderModule } from 'ng2-nouislider';
|
||||
import {
|
||||
NgbDatepickerModule,
|
||||
NgbDropdownModule,
|
||||
NgbNavModule,
|
||||
NgbPaginationModule,
|
||||
NgbTimepickerModule,
|
||||
NgbTooltipModule,
|
||||
NgbTypeaheadModule
|
||||
NgbDatepickerModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbTimepickerModule, NgbTooltipModule,
|
||||
NgbTypeaheadModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { MissingTranslationHandler, TranslateModule } from '@ngx-translate/core';
|
||||
import { NgxPaginationModule } from 'ngx-pagination';
|
||||
@@ -177,6 +172,7 @@ import { ScopeSelectorModalComponent } from './search-form/scope-selector-modal/
|
||||
import { BitstreamRequestACopyPageComponent } from './bitstream-request-a-copy-page/bitstream-request-a-copy-page.component';
|
||||
import { DsSelectComponent } from './ds-select/ds-select.component';
|
||||
import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component';
|
||||
import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component';
|
||||
|
||||
const MODULES = [
|
||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||
@@ -252,6 +248,7 @@ const COMPONENTS = [
|
||||
UploaderComponent,
|
||||
FileDropzoneNoUploaderComponent,
|
||||
ItemListPreviewComponent,
|
||||
ThemedItemListPreviewComponent,
|
||||
MyDSpaceItemStatusComponent,
|
||||
ItemSubmitterComponent,
|
||||
ItemDetailPreviewComponent,
|
||||
|
@@ -46,6 +46,9 @@ export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges
|
||||
// if an input or output has changed
|
||||
if (this.inAndOutputNames.some((name: any) => hasValue(changes[name]))) {
|
||||
this.connectInputsAndOutputs();
|
||||
if (this.compRef?.instance && 'ngOnChanges' in this.compRef?.instance) {
|
||||
(this.compRef.instance as any).ngOnChanges(changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -23,9 +23,7 @@ import { SubmissionFormsConfigService } from '../../../core/config/submission-fo
|
||||
import { SectionDataObject } from '../models/section-data.model';
|
||||
import { SectionsType } from '../sections-type';
|
||||
import {
|
||||
mockSubmissionCollectionId,
|
||||
mockSubmissionId,
|
||||
mockUploadResponse1ParsedErrors
|
||||
mockSubmissionCollectionId, mockSubmissionId, mockUploadResponse1ParsedErrors,
|
||||
} from '../../../shared/mocks/submission.mock';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
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 { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
|
||||
|
||||
function getMockSubmissionFormsConfigService(): SubmissionFormsConfigService {
|
||||
return jasmine.createSpyObj('FormOperationsService', {
|
||||
@@ -296,8 +295,10 @@ describe('SubmissionSectionFormComponent test suite', () => {
|
||||
};
|
||||
compAsAny.formData = {};
|
||||
compAsAny.sectionMetadata = ['dc.title'];
|
||||
spyOn(compAsAny, 'inCurrentSubmissionScope').and.callThrough();
|
||||
|
||||
expect(comp.hasMetadataEnrichment(newSectionData)).toBeTruthy();
|
||||
expect(compAsAny.inCurrentSubmissionScope).toHaveBeenCalledWith('dc.title');
|
||||
});
|
||||
|
||||
it('should return false when has not Metadata Enrichment', () => {
|
||||
@@ -306,7 +307,10 @@ describe('SubmissionSectionFormComponent test suite', () => {
|
||||
};
|
||||
compAsAny.formData = newSectionData;
|
||||
compAsAny.sectionMetadata = ['dc.title'];
|
||||
spyOn(compAsAny, 'inCurrentSubmissionScope').and.callThrough();
|
||||
|
||||
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', () => {
|
||||
@@ -318,6 +322,77 @@ describe('SubmissionSectionFormComponent test suite', () => {
|
||||
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', () => {
|
||||
spyOn(comp, 'initForm');
|
||||
spyOn(comp, 'checksForErrors');
|
||||
|
@@ -34,6 +34,9 @@ import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { ConfigObject } from '../../../core/config/models/config.model';
|
||||
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.
|
||||
@@ -112,7 +115,7 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
|
||||
*/
|
||||
protected subs: Subscription[] = [];
|
||||
|
||||
protected workspaceItem: WorkspaceItem;
|
||||
protected submissionObject: SubmissionObject;
|
||||
/**
|
||||
* The FormComponent reference
|
||||
*/
|
||||
@@ -173,10 +176,10 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
|
||||
getRemoteDataPayload())
|
||||
])),
|
||||
take(1))
|
||||
.subscribe(([sectionData, workspaceItem]: [WorkspaceitemSectionFormObject, WorkspaceItem]) => {
|
||||
.subscribe(([sectionData, submissionObject]: [WorkspaceitemSectionFormObject, SubmissionObject]) => {
|
||||
if (isUndefined(this.formModel)) {
|
||||
// this.sectionData.errorsToShow = [];
|
||||
this.workspaceItem = workspaceItem;
|
||||
this.submissionObject = submissionObject;
|
||||
// Is the first loading so init form
|
||||
this.initForm(sectionData);
|
||||
this.sectionData.data = sectionData;
|
||||
@@ -223,7 +226,7 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
|
||||
|
||||
const sectionDataToCheck = {};
|
||||
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];
|
||||
}
|
||||
});
|
||||
@@ -246,6 +249,28 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
|
||||
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
|
||||
*
|
||||
|
@@ -12,6 +12,7 @@ import { RemoteData } from '../core/data/remote-data';
|
||||
selector: 'ds-thumbnail',
|
||||
styleUrls: ['./thumbnail.component.scss'],
|
||||
templateUrl: './thumbnail.component.html',
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
export class ThumbnailComponent implements OnChanges {
|
||||
/**
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -559,6 +559,7 @@
|
||||
|
||||
"bitstream.download.page": "Now downloading {{bitstream}}..." ,
|
||||
|
||||
"bitstream.download.page.back": "Back" ,
|
||||
|
||||
|
||||
"bitstream.edit.authorizations.link": "Edit bitstream's Policies",
|
||||
|
@@ -4120,7 +4120,7 @@
|
||||
"search.filters.filter.dateSubmitted.placeholder": "Tallennnusajankohta",
|
||||
|
||||
// "search.filters.filter.discoverable.head": "Private",
|
||||
"search.filters.filter.discoverable.head": "Ykstyinen",
|
||||
"search.filters.filter.discoverable.head": "Yksityinen",
|
||||
|
||||
// "search.filters.filter.withdrawn.head": "Withdrawn",
|
||||
"search.filters.filter.withdrawn.head": "Poistettu käytöstä",
|
||||
|
@@ -1622,6 +1622,19 @@
|
||||
// "dso-selector.placeholder": "Search for a {{ 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": "Exporter métadonnées de {{ dsoName }}",
|
||||
@@ -4282,11 +4295,31 @@
|
||||
// "sorting.dc.title.DESC": "Title Descending",
|
||||
"sorting.dc.title.DESC": "Titre décroissant",
|
||||
|
||||
// "sorting.score.DESC": "Relevance",
|
||||
"sorting.score.DESC": "Pertinence",
|
||||
|
||||
|
||||
// "sorting.score.ASC": "Least Relevant",
|
||||
"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": "Statistiques",
|
||||
|
||||
|
@@ -3,7 +3,6 @@ import { makeStateKey } from '@angular/platform-browser';
|
||||
import { Config } from './config.interface';
|
||||
import { ServerConfig } from './server-config.interface';
|
||||
import { CacheConfig } from './cache-config.interface';
|
||||
import { UniversalConfig } from './universal-config.interface';
|
||||
import { INotificationBoardOptions } from './notifications-config.interfaces';
|
||||
import { SubmissionConfig } from './submission-config.interface';
|
||||
import { FormConfig } from './form-config.interfaces';
|
||||
@@ -25,7 +24,6 @@ interface AppConfig extends Config {
|
||||
form: FormConfig;
|
||||
notifications: INotificationBoardOptions;
|
||||
submission: SubmissionConfig;
|
||||
universal: UniversalConfig;
|
||||
debug: boolean;
|
||||
defaultLanguage: string;
|
||||
languages: LangConfig[];
|
||||
|
6
src/config/build-config.interface.ts
Normal file
6
src/config/build-config.interface.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { AppConfig } from './app-config.interface';
|
||||
import { UniversalConfig } from './universal-config.interface';
|
||||
|
||||
export interface BuildConfig extends AppConfig {
|
||||
universal: UniversalConfig;
|
||||
}
|
@@ -14,18 +14,10 @@ import { ServerConfig } from './server-config.interface';
|
||||
import { SubmissionConfig } from './submission-config.interface';
|
||||
import { ThemeConfig } from './theme.model';
|
||||
import { UIServerConfig } from './ui-server-config.interface';
|
||||
import { UniversalConfig } from './universal-config.interface';
|
||||
|
||||
export class DefaultAppConfig implements AppConfig {
|
||||
production = false;
|
||||
|
||||
// Angular Universal settings
|
||||
universal: UniversalConfig = {
|
||||
preboot: true,
|
||||
async: true,
|
||||
time: false
|
||||
};
|
||||
|
||||
// NOTE: will log all redux actions and transfers in console
|
||||
debug = false;
|
||||
|
||||
|
5
src/environments/.gitignore
vendored
Normal file
5
src/environments/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
environment.*.ts
|
||||
|
||||
!environment.production.ts
|
||||
!environment.test.ts
|
||||
!environment.ts
|
@@ -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,
|
||||
|
||||
// Angular Universal settings
|
||||
|
@@ -1,9 +1,9 @@
|
||||
// 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 { 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,
|
||||
|
||||
// Angular Universal settings
|
||||
|
@@ -3,14 +3,14 @@
|
||||
// `ng test --configuration test` replaces `environment.ts` with `environment.test.ts`.
|
||||
// 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,
|
||||
|
||||
// Angular Universal settings
|
||||
universal: {
|
||||
preboot: true,
|
||||
preboot: false,
|
||||
async: true,
|
||||
time: false
|
||||
}
|
||||
|
@@ -30,7 +30,9 @@ const main = () => {
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
if (hasValue(environment.universal) && environment.universal.preboot) {
|
||||
return bootstrap();
|
||||
} else {
|
||||
|
||||
@@ -47,7 +49,7 @@ const main = () => {
|
||||
};
|
||||
|
||||
// support async tag or hmr
|
||||
if (hasValue(environment.universal) && environment.universal.preboot === false) {
|
||||
if (hasValue(environment.universal) && !environment.universal.preboot) {
|
||||
main();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', main);
|
||||
|
@@ -19,7 +19,7 @@
|
||||
min-width: 75px;
|
||||
max-width: 150px;
|
||||
height: $height;
|
||||
background: linear-gradient(to right, rgba(255, 255, 255, 0), $bg 70%);
|
||||
background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba($bg, 1) 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,15 @@ $h4-factor: strip-unit($h4-font-size);
|
||||
.ds-hover .clamp-primary-#{$i} {
|
||||
@include clamp-with-titles($i, darken($primary, 10%));
|
||||
}
|
||||
|
||||
.clamp-light-#{$i} {
|
||||
@include clamp-with-titles($i, $light);
|
||||
}
|
||||
|
||||
:focus .clamp-light-#{$i},
|
||||
.ds-hover .clamp-light-#{$i} {
|
||||
@include clamp-with-titles($i, darken($light, 10%));
|
||||
}
|
||||
}
|
||||
|
||||
.clamp-none {
|
||||
|
@@ -0,0 +1,18 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommunityListComponent as BaseComponent } from '../../../../../app/community-list-page/community-list/community-list.component';
|
||||
|
||||
/**
|
||||
* A tree-structured list of nodes representing the communities, their subCommunities and collections.
|
||||
* Initially only the page-restricted top communities are shown.
|
||||
* Each node can be expanded to show its children and all children are also page-limited.
|
||||
* More pages of a page-limited result can be shown by pressing a show more node/link.
|
||||
* Which nodes were expanded is kept in the store, so this persists across pages.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-community-list',
|
||||
// styleUrls: ['./community-list.component.scss'],
|
||||
// templateUrl: './community-list.component.html'
|
||||
templateUrl: '../../../../../app/community-list-page/community-list/community-list.component.html'
|
||||
})
|
||||
export class CommunityListComponent extends BaseComponent {}
|
||||
|
@@ -1,11 +1,19 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { SearchPageComponent as BaseComponent } from '../../../../app/search-page/search-page.component';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../../app/my-dspace-page/my-dspace-page.component';
|
||||
import { SearchConfigurationService } from '../../../../app/core/shared/search/search-configuration.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-page',
|
||||
// styleUrls: ['./search-page.component.scss'],
|
||||
// templateUrl: './search-page.component.html'
|
||||
templateUrl: '../../../../app/search-page/search-page.component.html'
|
||||
templateUrl: '../../../../app/search-page/search-page.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
|
@@ -84,6 +84,7 @@ import { SearchModule } from '../../app/shared/search/search.module';
|
||||
import { ResourcePoliciesModule } from '../../app/shared/resource-policies/resource-policies.module';
|
||||
import { ComcolModule } from '../../app/shared/comcol/comcol.module';
|
||||
import { FeedbackComponent } from './app/info/feedback/feedback.component';
|
||||
import { CommunityListComponent } from './app/community-list-page/community-list/community-list.component';
|
||||
|
||||
const DECLARATIONS = [
|
||||
FileSectionComponent,
|
||||
@@ -126,7 +127,8 @@ const DECLARATIONS = [
|
||||
NavbarComponent,
|
||||
HeaderNavbarWrapperComponent,
|
||||
BreadcrumbsComponent,
|
||||
FeedbackComponent
|
||||
FeedbackComponent,
|
||||
CommunityListComponent
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
Reference in New Issue
Block a user