mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge remote-tracking branch 'upstream/master' into w2p-66391_ComCol-Tree---PR-Feedback
This commit is contained in:
97
README.md
97
README.md
@@ -3,13 +3,12 @@
|
||||
dspace-angular
|
||||
==============
|
||||
|
||||
> The next UI for DSpace, based on Angular Universal.
|
||||
> The next UI for DSpace 7, based on Angular Universal.
|
||||
|
||||
This project is currently in pre-alpha.
|
||||
This project is currently under active development. For more information on the DSpace 7 release see the [DSpace 7.0 Release Status wiki page](https://wiki.lyrasis.org/display/DSPACE/DSpace+Release+7.0+Status)
|
||||
|
||||
You can find additional information on the [wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+-+Angular+UI) or [the project board (waffle.io)](https://waffle.io/DSpace/dspace-angular).
|
||||
You can find additional information on the DSpace 7 Angular UI on the [wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+-+Angular+UI+Development).
|
||||
|
||||
If you're looking for the 2016 Angular 2 DSpace UI prototype, you can find it [here](https://github.com/DSpace-Labs/angular2-ui-prototype)
|
||||
|
||||
Quick start
|
||||
-----------
|
||||
@@ -32,8 +31,6 @@ yarn start
|
||||
|
||||
Then go to [http://localhost:3000](http://localhost:3000) in your browser
|
||||
|
||||
NOTE: currently there's not much to see at that URL. We really do need your help. If you're interested in jumping in, and you've made it this far, please look at the [the project board (waffle.io)](https://waffle.io/DSpace/dspace-angular), grab a card, and get to work. Thanks!
|
||||
|
||||
Not sure where to start? watch the training videos linked in the [Introduction to the technology](#introduction-to-the-technology) section below.
|
||||
|
||||
Table of Contents
|
||||
@@ -42,24 +39,27 @@ Table of Contents
|
||||
- [Introduction to the technology](#introduction-to-the-technology)
|
||||
- [Requirements](#requirements)
|
||||
- [Installing](#installing)
|
||||
- [Configuring](#configuring)
|
||||
- [Configuring](#configuring)
|
||||
- [Running the app](#running-the-app)
|
||||
- [Running in production mode](#running-in-production-mode)
|
||||
- [Running in production mode](#running-in-production-mode)
|
||||
- [Deploy](#deploy)
|
||||
- [Running the application with Docker](#running-the-application-with-docker)
|
||||
- [Cleaning](#cleaning)
|
||||
- [Testing](#testing)
|
||||
- [Test a Pull Request](#test-a-pull-request)
|
||||
- [Documentation](#documentation)
|
||||
- [Other commands](#other-commands)
|
||||
- [Recommended Editors/IDEs](#recommended-editorsides)
|
||||
- [Collaborating](#collaborating)
|
||||
- [File Structure](#file-structure)
|
||||
- [3rd Party Library Installation](#3rd-party-library-installation)
|
||||
- [Managing Dependencies (via yarn)](#managing-dependencies-via-yarn)
|
||||
- [Frequently asked questions](#frequently-asked-questions)
|
||||
- [License](#license)
|
||||
|
||||
Introduction to the technology
|
||||
------------------------------
|
||||
|
||||
You can find more information on the technologies used in this project (Angular 2, Typescript, Angular Universal, RxJS, etc) on the [DuraSpace wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+UI+Technology+Stack)
|
||||
You can find more information on the technologies used in this project (Angular.io, Typescript, Angular Universal, RxJS, etc) on the [LYRASIS wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+UI+Technology+Stack)
|
||||
|
||||
Requirements
|
||||
------------
|
||||
@@ -75,8 +75,7 @@ Installing
|
||||
- `yarn run global` to install the required global dependencies
|
||||
- `yarn install` to install the local dependencies
|
||||
|
||||
Configuring
|
||||
-----------
|
||||
### Configuring
|
||||
|
||||
Default configuration file is located in `config/` folder.
|
||||
|
||||
@@ -98,8 +97,7 @@ Running the app
|
||||
|
||||
After you have installed all dependencies you can now run the app. Run `yarn run watch` to start a local server which will watch for changes, rebuild the code, and reload the server for you. You can visit it at `http://localhost:3000`.
|
||||
|
||||
Running in production mode
|
||||
--------------------------
|
||||
### Running in production mode
|
||||
|
||||
When building for production we're using Ahead of Time (AoT) compilation. With AoT, the browser downloads a pre-compiled version of the application, so it can render the application immediately, without waiting to compile the app first. The compiler is roughly half the size of Angular itself, so omitting it dramatically reduces the application payload.
|
||||
|
||||
@@ -117,6 +115,19 @@ yarn run build:prod
|
||||
|
||||
This will build the application and put the result in the `dist` folder
|
||||
|
||||
### Deploy
|
||||
```bash
|
||||
# deploy production in standalone pm2 container
|
||||
yarn run deploy
|
||||
|
||||
# remove production from standalone pm2 container
|
||||
yarn run undeploy
|
||||
```
|
||||
|
||||
### Running the application with Docker
|
||||
See [Docker Runtime Options](docker/README.md)
|
||||
|
||||
|
||||
Cleaning
|
||||
--------
|
||||
|
||||
@@ -131,10 +142,6 @@ yarn run clean:prod
|
||||
yarn run clean:dist
|
||||
```
|
||||
|
||||
Running the application with Docker
|
||||
-----------------------------------
|
||||
See [Docker Runtime Options](docker/README.md)
|
||||
|
||||
|
||||
Testing
|
||||
-------
|
||||
@@ -189,21 +196,14 @@ To run all the tests (e.g.: to run tests with Continuous Integration software) y
|
||||
Documentation
|
||||
--------------
|
||||
|
||||
See [`./docs`](docs) for further documentation.
|
||||
|
||||
### Building code documentation
|
||||
|
||||
To build the code documentation we use [TYPEDOC](http://typedoc.org). TYPEDOC is a documentation generator for TypeScript projects. It extracts informations from properly formatted comments that can be written within the code files. Follow the instructions [here](http://typedoc.org/guides/doccomments/) to know how to make those comments.
|
||||
|
||||
Run:`yarn run docs` to produce the documentation that will be available in the 'doc' folder.
|
||||
|
||||
Deploy
|
||||
------
|
||||
|
||||
```bash
|
||||
# deploy production in standalone pm2 container
|
||||
yarn run deploy
|
||||
|
||||
# remove production from standalone pm2 container
|
||||
yarn run undeploy
|
||||
```
|
||||
|
||||
Other commands
|
||||
--------------
|
||||
|
||||
@@ -229,7 +229,7 @@ To get the most out of TypeScript, you'll need a TypeScript-aware editor. We've
|
||||
Collaborating
|
||||
-------------
|
||||
|
||||
See [the guide on the wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+-+Angular+2+UI#DSpace7-Angular2UI-Howtocontribute)
|
||||
See [the guide on the wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+-+Angular+UI+Development#DSpace7-AngularUIDevelopment-Howtocontribute)
|
||||
|
||||
File Structure
|
||||
--------------
|
||||
@@ -335,10 +335,20 @@ dspace-angular
|
||||
└── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock)
|
||||
```
|
||||
|
||||
3rd Party Library Installation
|
||||
------------------------------
|
||||
Managing Dependencies (via yarn)
|
||||
-------------
|
||||
|
||||
Install your library via `yarn add lib-name --save` and import it in your code. `--save` will add it to `package.json`.
|
||||
This project makes use of [`yarn`](https://yarnpkg.com/en/) to ensure that the exact same dependency versions are used every time you install it.
|
||||
|
||||
* `yarn` creates a [`yarn.lock`](https://yarnpkg.com/en/docs/yarn-lock) to track those versions. That file is updated automatically by whenever dependencies are added/updated/removed via yarn.
|
||||
* **Adding new dependencies**: To install/add a new dependency (third party library), use [`yarn add`](https://yarnpkg.com/en/docs/cli/add). For example: `yarn add some-lib`.
|
||||
* If you are adding a new build tool dependency (to `devDependencies`), use `yarn add some-lib --dev`
|
||||
* **Upgrading existing dependencies**: To upgrade existing dependencies, you can use [`yarn upgrade`](https://yarnpkg.com/en/docs/cli/upgrade). For example: `yarn upgrade some-lib` or `yarn upgrade some-lib@version`
|
||||
* **Removing dependencies**: If a dependency is no longer needed, or replaced, use [`yarn remove`](https://yarnpkg.com/en/docs/cli/remove) to remove it.
|
||||
|
||||
As you can see above, using `yarn` commandline tools means that you should never need to modify the `package.json` manually. *We recommend always using `yarn` to keep dependencies updated / in sync.*
|
||||
|
||||
### Adding Typings for libraries
|
||||
|
||||
If the library does not include typings, you can install them using yarn:
|
||||
|
||||
@@ -370,24 +380,6 @@ If you're importing a module that uses CommonJS you need to import as
|
||||
import * as _ from 'lodash';
|
||||
```
|
||||
|
||||
Managing Dependencies (via yarn)
|
||||
-------------
|
||||
|
||||
This project makes use of [`yarn`](https://yarnpkg.com/en/) to ensure that the exact same dependency versions are used every time you install it.
|
||||
|
||||
* `yarn` creates a [`yarn.lock`](https://yarnpkg.com/en/docs/yarn-lock) to track those versions. That file is updated automatically by whenever dependencies are added/updated/removed via yarn.
|
||||
* **Adding new dependencies**: To install/add a new dependency (third party library), use [`yarn add`](https://yarnpkg.com/en/docs/cli/add). For example: `yarn add some-lib`.
|
||||
* If you are adding a new build tool dependency (to `devDependencies`), use `yarn add some-lib --dev`
|
||||
* **Upgrading existing dependencies**: To upgrade existing dependencies, you can use [`yarn upgrade`](https://yarnpkg.com/en/docs/cli/upgrade). For example: `yarn upgrade some-lib` or `yarn upgrade some-lib@version`
|
||||
* **Removing dependencies**: If a dependency is no longer needed, or replaced, use [`yarn remove`](https://yarnpkg.com/en/docs/cli/remove) to remove it.
|
||||
|
||||
As you can see above, using `yarn` commandline tools means that you should never need to modify the `package.json` manually. *We recommend always using `yarn` to keep dependencies updated / in sync.*
|
||||
|
||||
Further Documentation
|
||||
---------------------
|
||||
|
||||
See [`./docs`](docs) for further documentation.
|
||||
|
||||
Frequently asked questions
|
||||
--------------------------
|
||||
|
||||
@@ -411,5 +403,4 @@ Frequently asked questions
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
http://www.dspace.org/license
|
||||
This project's source code is made available under the DSpace BSD License: http://www.dspace.org/license
|
||||
|
@@ -2,7 +2,8 @@ import { browser, element, by } from 'protractor';
|
||||
|
||||
export class ProtractorPage {
|
||||
navigateTo() {
|
||||
return browser.get('/');
|
||||
return browser.get('/')
|
||||
.then(() => browser.waitForAngular());
|
||||
}
|
||||
|
||||
getPageTitleText() {
|
||||
|
@@ -229,7 +229,7 @@
|
||||
"rollup-plugin-node-globals": "1.2.1",
|
||||
"rollup-plugin-node-resolve": "^3.0.3",
|
||||
"rollup-plugin-terser": "^2.0.2",
|
||||
"sass-loader": "7.1.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"script-ext-html-webpack-plugin": "2.0.1",
|
||||
"source-map": "0.7.3",
|
||||
"source-map-loader": "0.2.4",
|
||||
|
3
resources/fonts/README.md
Normal file
3
resources/fonts/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Supported font formats
|
||||
|
||||
DSpace supports EOT, TTF, OTF, SVG, WOFF and WOFF2 fonts.
|
@@ -3,6 +3,7 @@
|
||||
*ngVar="(collectionRD$ | async) as collectionRD">
|
||||
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="collectionRD?.payload as collection">
|
||||
<ds-view-tracker [object]="collection"></ds-view-tracker>
|
||||
<header class="comcol-header border-bottom mb-4 pb-4">
|
||||
<!-- Collection logo -->
|
||||
<ds-comcol-page-logo *ngIf="logoRD$"
|
||||
|
@@ -12,12 +12,14 @@ import { DeleteCollectionPageComponent } from './delete-collection-page/delete-c
|
||||
import { SearchService } from '../+search-page/search-service/search.service';
|
||||
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
||||
import { SearchFixedFilterService } from '../+search-page/search-filters/search-filter/search-fixed-filter.service';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
CollectionPageRoutingModule
|
||||
CollectionPageRoutingModule,
|
||||
StatisticsModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
CollectionPageComponent,
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<div class="container" *ngVar="(communityRD$ | async) as communityRD">
|
||||
<div class="community-page" *ngIf="communityRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="communityRD?.payload; let communityPayload">
|
||||
<ds-view-tracker [object]="communityPayload"></ds-view-tracker>
|
||||
<header class="comcol-header border-bottom mb-4 pb-4">
|
||||
<!-- Community logo -->
|
||||
<ds-comcol-page-logo *ngIf="logoRD$" [logo]="(logoRD$ | async)?.payload" [alternateText]="'Community Logo'">
|
||||
|
@@ -6,17 +6,19 @@ import { SharedModule } from '../shared/shared.module';
|
||||
import { CommunityPageComponent } from './community-page.component';
|
||||
import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component';
|
||||
import { CommunityPageRoutingModule } from './community-page-routing.module';
|
||||
import {CommunityPageSubCommunityListComponent} from './sub-community-list/community-page-sub-community-list.component';
|
||||
import { CommunityPageSubCommunityListComponent } from './sub-community-list/community-page-sub-community-list.component';
|
||||
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
|
||||
import { CommunityFormComponent } from './community-form/community-form.component';
|
||||
import { EditCommunityPageComponent } from './edit-community-page/edit-community-page.component';
|
||||
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
CommunityPageRoutingModule
|
||||
CommunityPageRoutingModule,
|
||||
StatisticsModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
CommunityPageComponent,
|
||||
|
@@ -2,12 +2,25 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { HomePageComponent } from './home-page.component';
|
||||
import { HomePageResolver } from './home-page.resolver';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{ path: '', component: HomePageComponent, pathMatch: 'full', data: { title: 'home.title' } }
|
||||
{
|
||||
path: '',
|
||||
component: HomePageComponent,
|
||||
pathMatch: 'full',
|
||||
data: {title: 'home.title'},
|
||||
resolve: {
|
||||
site: HomePageResolver
|
||||
}
|
||||
}
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
HomePageResolver
|
||||
]
|
||||
})
|
||||
export class HomePageRoutingModule { }
|
||||
export class HomePageRoutingModule {
|
||||
}
|
||||
|
@@ -1,5 +1,8 @@
|
||||
<ds-home-news></ds-home-news>
|
||||
<div class="container">
|
||||
<ng-container *ngIf="(site$ | async) as site">
|
||||
<ds-view-tracker [object]="site"></ds-view-tracker>
|
||||
</ng-container>
|
||||
<ds-search-form [inPlaceSearch]="false"></ds-search-form>
|
||||
<ds-top-level-community-list></ds-top-level-community-list>
|
||||
</div>
|
||||
|
@@ -1,9 +1,26 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Site } from '../core/shared/site.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-home-page',
|
||||
styleUrls: ['./home-page.component.scss'],
|
||||
templateUrl: './home-page.component.html'
|
||||
})
|
||||
export class HomePageComponent {
|
||||
export class HomePageComponent implements OnInit {
|
||||
|
||||
site$:Observable<Site>;
|
||||
|
||||
constructor(
|
||||
private route:ActivatedRoute,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit():void {
|
||||
this.site$ = this.route.data.pipe(
|
||||
map((data) => data.site as Site),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -6,12 +6,14 @@ import { HomePageRoutingModule } from './home-page-routing.module';
|
||||
|
||||
import { HomePageComponent } from './home-page.component';
|
||||
import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
HomePageRoutingModule
|
||||
HomePageRoutingModule,
|
||||
StatisticsModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
HomePageComponent,
|
||||
|
25
src/app/+home-page/home-page.resolver.ts
Normal file
25
src/app/+home-page/home-page.resolver.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||
import { SiteDataService } from '../core/data/site-data.service';
|
||||
import { Site } from '../core/shared/site.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
/**
|
||||
* The class that resolve the Site object for a route
|
||||
*/
|
||||
@Injectable()
|
||||
export class HomePageResolver implements Resolve<Site> {
|
||||
constructor(private siteService:SiteDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for resolving a site object
|
||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||
* @returns Observable<Site> Emits the found Site object, or an error if something went wrong
|
||||
*/
|
||||
resolve(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<Site> | Promise<Site> | Site {
|
||||
return this.siteService.find().pipe(take(1));
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="itemRD?.payload as item">
|
||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
|
||||
<div class="simple-view-link my-3">
|
||||
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id]">
|
||||
|
@@ -26,6 +26,7 @@ import { MetadataRepresentationListComponent } from './simple/metadata-represent
|
||||
import { RelatedEntitiesSearchComponent } from './simple/related-entities/related-entities-search/related-entities-search.component';
|
||||
import { MetadataValuesComponent } from './field-components/metadata-values/metadata-values.component';
|
||||
import { MetadataFieldWrapperComponent } from './field-components/metadata-field-wrapper/metadata-field-wrapper.component';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -33,7 +34,8 @@ import { MetadataFieldWrapperComponent } from './field-components/metadata-field
|
||||
SharedModule,
|
||||
ItemPageRoutingModule,
|
||||
EditItemPageModule,
|
||||
SearchPageModule
|
||||
SearchPageModule,
|
||||
StatisticsModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
ItemPageComponent,
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<div class="container" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
|
||||
<div *ngIf="itemRD?.payload as item">
|
||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1 +1 @@
|
||||
@import '../+search-page/search-page.component.scss';
|
||||
@import '../+search-page/search.component.scss';
|
||||
|
@@ -22,7 +22,7 @@ import { SearchConfigurationServiceStub } from '../shared/testing/search-configu
|
||||
import { SearchService } from '../+search-page/search-service/search.service';
|
||||
import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service';
|
||||
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||
import { SearchSidebarService } from '../+search-page/search-sidebar/search-sidebar.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SearchFilterService } from '../+search-page/search-filters/search-filter/search-filter.service';
|
||||
import { RoleDirective } from '../shared/roles/role.directive';
|
||||
import { RoleService } from '../core/roles/role.service';
|
||||
@@ -109,7 +109,7 @@ describe('MyDSpacePageComponent', () => {
|
||||
})
|
||||
},
|
||||
{
|
||||
provide: SearchSidebarService,
|
||||
provide: SidebarService,
|
||||
useValue: sidebarService
|
||||
},
|
||||
{
|
||||
|
@@ -17,7 +17,7 @@ import { pushInOut } from '../shared/animations/push';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||
import { SearchService } from '../+search-page/search-service/search.service';
|
||||
import { SearchSidebarService } from '../+search-page/search-sidebar/search-sidebar.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service';
|
||||
@@ -102,7 +102,7 @@ export class MyDSpacePageComponent implements OnInit {
|
||||
context$: Observable<Context>;
|
||||
|
||||
constructor(private service: SearchService,
|
||||
private sidebarService: SearchSidebarService,
|
||||
private sidebarService: SidebarService,
|
||||
private windowService: HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService) {
|
||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { configureSearchComponentTestingModule } from './search-page.component.spec';
|
||||
import { configureSearchComponentTestingModule } from './search.component.spec';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { pushInOut } from '../shared/animations/push';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
@@ -16,8 +16,8 @@ import { RouteService } from '../core/services/route.service';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-configuration-search-page',
|
||||
styleUrls: ['./search-page.component.scss'],
|
||||
templateUrl: './search-page.component.html',
|
||||
styleUrls: ['./search.component.scss'],
|
||||
templateUrl: './search.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [pushInOut],
|
||||
providers: [
|
||||
@@ -28,7 +28,7 @@ import { RouteService } from '../core/services/route.service';
|
||||
]
|
||||
})
|
||||
|
||||
export class ConfigurationSearchPageComponent extends SearchPageComponent implements OnInit {
|
||||
export class ConfigurationSearchPageComponent extends SearchComponent implements OnInit {
|
||||
/**
|
||||
* The configuration to use for the search options
|
||||
* If empty, the configuration will be determined by the route parameter called 'configuration'
|
||||
@@ -36,7 +36,7 @@ export class ConfigurationSearchPageComponent extends SearchPageComponent implem
|
||||
@Input() configuration: string;
|
||||
|
||||
constructor(protected service: SearchService,
|
||||
protected sidebarService: SearchSidebarService,
|
||||
protected sidebarService: SidebarService,
|
||||
protected windowService: HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||
protected routeService: RouteService) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { FilteredSearchPageComponent } from './filtered-search-page.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { configureSearchComponentTestingModule } from './search-page.component.spec';
|
||||
import { configureSearchComponentTestingModule } from './search.component.spec';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
|
||||
describe('FilteredSearchPageComponent', () => {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { pushInOut } from '../shared/animations/push';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
@@ -18,8 +18,8 @@ import { RouteService } from '../core/services/route.service';
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-filtered-search-page',
|
||||
styleUrls: ['./search-page.component.scss'],
|
||||
templateUrl: './search-page.component.html',
|
||||
styleUrls: ['./search.component.scss'],
|
||||
templateUrl: './search.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [pushInOut],
|
||||
providers: [
|
||||
@@ -30,7 +30,7 @@ import { RouteService } from '../core/services/route.service';
|
||||
]
|
||||
})
|
||||
|
||||
export class FilteredSearchPageComponent extends SearchPageComponent implements OnInit {
|
||||
export class FilteredSearchPageComponent extends SearchComponent implements OnInit {
|
||||
/**
|
||||
* The actual query for the fixed filter.
|
||||
* If empty, the query will be determined by the route parameter called 'filter'
|
||||
@@ -38,7 +38,7 @@ export class FilteredSearchPageComponent extends SearchPageComponent implements
|
||||
@Input() fixedFilterQuery: string;
|
||||
|
||||
constructor(protected service: SearchService,
|
||||
protected sidebarService: SearchSidebarService,
|
||||
protected sidebarService: SidebarService,
|
||||
protected windowService: HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||
protected routeService: RouteService) {
|
||||
|
@@ -1,7 +1,18 @@
|
||||
<div class="facet-filter d-block mb-3 p-3" *ngIf="active$ | async">
|
||||
<div (click)="toggle()" class="filter-name"><h5 class="d-inline-block mb-0">{{'search.filters.filter.' + filter.name + '.head'| translate}}</h5> <span class="filter-toggle fas float-right"
|
||||
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'"></span></div>
|
||||
<div [@slide]="(collapsed$ | async) ? 'collapsed' : 'expanded'" (@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)" class="search-filter-wrapper" [ngClass]="{'closed' : closed}">
|
||||
<ds-search-facet-filter-wrapper [filterConfig]="filter" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-filter-wrapper>
|
||||
</div>
|
||||
<div (click)="toggle()" class="filter-name">
|
||||
<h5 class="d-inline-block mb-0">
|
||||
{{'search.filters.filter.' + filter.name + '.head'| translate}}
|
||||
</h5>
|
||||
<span class="filter-toggle fas float-right"
|
||||
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'">
|
||||
</span>
|
||||
</div>
|
||||
<div [@slide]="(collapsed$ | async) ? 'collapsed' : 'expanded'"
|
||||
(@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)"
|
||||
class="search-filter-wrapper" [ngClass]="{'closed' : closed}">
|
||||
<ds-search-facet-filter-wrapper
|
||||
[filterConfig]="filter"
|
||||
[inPlaceSearch]="inPlaceSearch">
|
||||
</ds-search-facet-filter-wrapper>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -138,7 +138,7 @@ describe('SearchFilterService', () => {
|
||||
service.expand(mockFilterConfig.name);
|
||||
});
|
||||
|
||||
it('SearchSidebarExpandAction should be dispatched to the store', () => {
|
||||
it('SidebarExpandAction should be dispatched to the store', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterExpandAction(mockFilterConfig.name));
|
||||
});
|
||||
});
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@@ -1,43 +1,2 @@
|
||||
<div class="container">
|
||||
<div class="search-page row">
|
||||
<ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-{{sideBarWidth}} sidebar-md-sticky"
|
||||
id="search-sidebar"
|
||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements" [inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||
<div class="col-12 col-md-{{12 - sideBarWidth}}">
|
||||
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
||||
[query]="(searchOptions$ | async)?.query"
|
||||
[scope]="(searchOptions$ | async)?.scope"
|
||||
[currentUrl]="searchLink"
|
||||
[scopes]="(scopeListRD$ | async)"
|
||||
[inPlaceSearch]="inPlaceSearch">
|
||||
</ds-search-form>
|
||||
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||
<div class="row">
|
||||
<div id="search-body"
|
||||
class="row-offcanvas row-offcanvas-left"
|
||||
[@pushInOut]="(isSidebarCollapsed$ | async) ? 'collapsed' : 'expanded'">
|
||||
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
|
||||
id="search-sidebar-sm"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
(toggleSidebar)="closeSidebar()"
|
||||
[ngClass]="{'active': !(isSidebarCollapsed$ | async)}">
|
||||
</ds-search-sidebar>
|
||||
<div id="search-content" class="col-12">
|
||||
<div class="d-block d-md-none search-controls clearfix">
|
||||
<ds-view-mode-switch [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
|
||||
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
||||
| translate}}
|
||||
</button>
|
||||
</div>
|
||||
<ds-search-results [searchResults]="resultsRD$ | async"
|
||||
[searchConfig]="searchOptions$ | async"
|
||||
[configuration]="configuration$ | async"
|
||||
[disableHeader]="!searchEnabled"></ds-search-results>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ds-search></ds-search>
|
||||
<ds-search-tracker></ds-search-tracker>
|
||||
|
@@ -1,52 +0,0 @@
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .search-controls {
|
||||
margin-bottom: $spacer;
|
||||
}
|
||||
|
||||
#search-body {
|
||||
&.row-offcanvas {
|
||||
width: 100%;
|
||||
}
|
||||
@include media-breakpoint-down(sm) {
|
||||
position: relative;
|
||||
|
||||
&.row-offcanvas {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&.row-offcanvas-right #search-sidebar-sm {
|
||||
right: -100%;
|
||||
}
|
||||
|
||||
&.row-offcanvas-left #search-sidebar-sm {
|
||||
left: -100%;
|
||||
}
|
||||
|
||||
#search-sidebar-sm {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
.sidebar-md-sticky {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
z-index: $zindex-sticky;
|
||||
padding-top: $content-spacing;
|
||||
margin-top: -$content-spacing;
|
||||
align-self: flex-start;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
@@ -1,184 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { startWith, switchMap, } from 'rxjs/operators';
|
||||
import { PaginatedList } from '../core/data/paginated-list';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||
import { pushInOut } from '../shared/animations/push';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||
import { SearchResult } from './search-result.model';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
import { RouteService } from '../core/services/route.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||
|
||||
export const SEARCH_ROUTE = '/search';
|
||||
|
||||
/**
|
||||
* This component renders a simple item page.
|
||||
* The route parameter 'id' is used to request the item it represents.
|
||||
* All fields of the item that should be displayed, are defined in its template.
|
||||
*/
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-page',
|
||||
styleUrls: ['./search-page.component.scss'],
|
||||
templateUrl: './search-page.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [pushInOut],
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* This component represents the whole search page
|
||||
* It renders search results depending on the current search options
|
||||
*/
|
||||
export class SearchPageComponent implements OnInit {
|
||||
/**
|
||||
* The current search results
|
||||
*/
|
||||
resultsRD$: BehaviorSubject<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> = new BehaviorSubject(null);
|
||||
|
||||
/**
|
||||
* The current paginated search options
|
||||
*/
|
||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||
|
||||
/**
|
||||
* The current relevant scopes
|
||||
*/
|
||||
scopeListRD$: Observable<DSpaceObject[]>;
|
||||
|
||||
/**
|
||||
* Emits true if were on a small screen
|
||||
*/
|
||||
isXsOrSm$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Subscription to unsubscribe from
|
||||
*/
|
||||
sub: Subscription;
|
||||
|
||||
/**
|
||||
* True when the search component should show results on the current page
|
||||
*/
|
||||
@Input() inPlaceSearch = true;
|
||||
|
||||
/**
|
||||
* Whether or not the search bar should be visible
|
||||
*/
|
||||
@Input()
|
||||
searchEnabled = true;
|
||||
|
||||
/**
|
||||
* The width of the sidebar (bootstrap columns)
|
||||
*/
|
||||
@Input()
|
||||
sideBarWidth = 3;
|
||||
|
||||
/**
|
||||
* The currently applied configuration (determines title of search)
|
||||
*/
|
||||
@Input()
|
||||
configuration$: Observable<string>;
|
||||
|
||||
/**
|
||||
* Link to the search page
|
||||
*/
|
||||
searchLink: string;
|
||||
|
||||
/**
|
||||
* Observable for whether or not the sidebar is currently collapsed
|
||||
*/
|
||||
isSidebarCollapsed$: Observable<boolean>;
|
||||
|
||||
constructor(protected service: SearchService,
|
||||
protected sidebarService: SearchSidebarService,
|
||||
protected windowService: HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||
protected routeService: RouteService) {
|
||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listening to changes in the paginated search options
|
||||
* If something changes, update the search results
|
||||
*
|
||||
* Listen to changes in the scope
|
||||
* If something changes, update the list of scopes for the dropdown
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.isSidebarCollapsed$ = this.isSidebarCollapsed();
|
||||
this.searchLink = this.getSearchLink();
|
||||
this.searchOptions$ = this.getSearchOptions();
|
||||
this.sub = this.searchOptions$.pipe(
|
||||
switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData(), startWith(undefined))))
|
||||
.subscribe((results) => {
|
||||
this.resultsRD$.next(results);
|
||||
});
|
||||
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||
switchMap((scopeId) => this.service.getScopes(scopeId))
|
||||
);
|
||||
if (!isNotEmpty(this.configuration$)) {
|
||||
this.configuration$ = this.routeService.getRouteParameterValue('configuration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current paginated search options
|
||||
* @returns {Observable<PaginatedSearchOptions>}
|
||||
*/
|
||||
protected getSearchOptions(): Observable<PaginatedSearchOptions> {
|
||||
return this.searchConfigService.paginatedSearchOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to a collapsed state
|
||||
*/
|
||||
public closeSidebar(): void {
|
||||
this.sidebarService.collapse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to an expanded state
|
||||
*/
|
||||
public openSidebar(): void {
|
||||
this.sidebarService.expand();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the sidebar is collapsed
|
||||
* @returns {Observable<boolean>} emits true if the sidebar is currently collapsed, false if it is expanded
|
||||
*/
|
||||
private isSidebarCollapsed(): Observable<boolean> {
|
||||
return this.sidebarService.isCollapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
|
||||
*/
|
||||
private getSearchLink(): string {
|
||||
if (this.inPlaceSearch) {
|
||||
return './';
|
||||
}
|
||||
return this.service.getSearchLink();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from the subscription
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.sub)) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
export class SearchPageComponent {
|
||||
}
|
||||
|
@@ -3,13 +3,13 @@ import { CommonModule } from '@angular/common';
|
||||
import { CoreModule } from '../core/core.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { SearchPageRoutingModule } from './search-page-routing.module';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { SearchResultsComponent } from './search-results/search-results.component';
|
||||
import { CommunitySearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'
|
||||
import { CollectionSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component';
|
||||
import { SearchSidebarComponent } from './search-sidebar/search-sidebar.component';
|
||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||
import { SearchSidebarEffects } from './search-sidebar/search-sidebar.effects';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SidebarEffects } from '../shared/sidebar/sidebar-effects.service';
|
||||
import { SearchSettingsComponent } from './search-settings/search-settings.component';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { SearchFiltersComponent } from './search-filters/search-filters.component';
|
||||
@@ -33,13 +33,18 @@ import { SearchLabelComponent } from './search-labels/search-label/search-label.
|
||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
||||
import { FilteredSearchPageComponent } from './filtered-search-page.component';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
import { SearchTrackerComponent } from './search-tracker.component';
|
||||
|
||||
const effects = [
|
||||
SearchSidebarEffects
|
||||
SidebarEffects
|
||||
];
|
||||
|
||||
const components = [
|
||||
SearchPageComponent,
|
||||
SearchComponent,
|
||||
SearchResultsComponent,
|
||||
SearchSidebarComponent,
|
||||
SearchSettingsComponent,
|
||||
@@ -60,7 +65,8 @@ const components = [
|
||||
SearchSwitchConfigurationComponent,
|
||||
SearchAuthorityFilterComponent,
|
||||
FilteredSearchPageComponent,
|
||||
ConfigurationSearchPageComponent
|
||||
ConfigurationSearchPageComponent,
|
||||
SearchTrackerComponent,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
@@ -69,11 +75,13 @@ const components = [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
EffectsModule.forFeature(effects),
|
||||
CoreModule.forRoot()
|
||||
CoreModule.forRoot(),
|
||||
StatisticsModule.forRoot(),
|
||||
],
|
||||
declarations: components,
|
||||
providers: [
|
||||
SearchSidebarService,
|
||||
SidebarService,
|
||||
SidebarFilterService,
|
||||
SearchFilterService,
|
||||
SearchFixedFilterService,
|
||||
ConfigurationSearchPageGuard,
|
||||
|
@@ -226,7 +226,7 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
this.getFiltersPart(),
|
||||
).subscribe((update) => {
|
||||
const currentValue: SearchOptions = this.searchOptions.getValue();
|
||||
const updatedValue: SearchOptions = Object.assign(currentValue, update);
|
||||
const updatedValue: SearchOptions = Object.assign(new SearchOptions({}), currentValue, update);
|
||||
this.searchOptions.next(updatedValue);
|
||||
});
|
||||
}
|
||||
@@ -247,7 +247,7 @@ export class SearchConfigurationService implements OnDestroy {
|
||||
this.getFiltersPart(),
|
||||
).subscribe((update) => {
|
||||
const currentValue: PaginatedSearchOptions = this.paginatedSearchOptions.getValue();
|
||||
const updatedValue: PaginatedSearchOptions = Object.assign(currentValue, update);
|
||||
const updatedValue: PaginatedSearchOptions = Object.assign(new PaginatedSearchOptions({}), currentValue, update);
|
||||
this.paginatedSearchOptions.next(updatedValue);
|
||||
});
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf, zip as observableZip } from 'rxjs';
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { Injectable, OnDestroy } from '@angular/core';
|
||||
import { NavigationExtras, PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router';
|
||||
import { first, map, switchMap, take, tap } from 'rxjs/operators';
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
getSucceededRemoteData
|
||||
} from '../../core/shared/operators';
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
||||
import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
||||
import { NormalizedSearchResult } from '../normalized-search-result.model';
|
||||
import { SearchOptions } from '../search-options.model';
|
||||
import { SearchResult } from '../search-result.model';
|
||||
@@ -42,6 +42,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||
import { RouteService } from '../../core/services/route.service';
|
||||
import { RequestEntry } from '../../core/data/request.reducer';
|
||||
|
||||
/**
|
||||
* Service that performs all general actions that have to do with the search page
|
||||
@@ -97,9 +98,9 @@ export class SearchService implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
getEndpoint(searchOptions?: PaginatedSearchOptions): Observable<string> {
|
||||
getEndpoint(searchOptions?:PaginatedSearchOptions):Observable<string> {
|
||||
return this.halService.getEndpoint(this.searchLinkPath).pipe(
|
||||
map((url: string) => {
|
||||
map((url:string) => {
|
||||
if (hasValue(searchOptions)) {
|
||||
return (searchOptions as PaginatedSearchOptions).toRestUrl(url);
|
||||
} else {
|
||||
@@ -116,32 +117,60 @@ export class SearchService implements OnDestroy {
|
||||
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
||||
*/
|
||||
search(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||
return this.getPaginatedResults(this.searchEntries(searchOptions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to retrieve request entries for search results from the server
|
||||
* @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search
|
||||
* @param responseMsToLive The amount of milliseconds for the response to live in cache
|
||||
* @returns {Observable<RequestEntry>} Emits an observable with the request entries
|
||||
*/
|
||||
searchEntries(searchOptions?: PaginatedSearchOptions, responseMsToLive?:number)
|
||||
:Observable<{searchOptions:PaginatedSearchOptions, requestEntry:RequestEntry}> {
|
||||
|
||||
const hrefObs = this.getEndpoint(searchOptions);
|
||||
|
||||
const requestObs = hrefObs.pipe(
|
||||
map((url: string) => {
|
||||
map((url:string) => {
|
||||
const request = new this.request(this.requestService.generateRequestId(), url);
|
||||
|
||||
const getResponseParserFn: () => GenericConstructor<ResponseParsingService> = () => {
|
||||
const getResponseParserFn:() => GenericConstructor<ResponseParsingService> = () => {
|
||||
return this.parser;
|
||||
};
|
||||
|
||||
return Object.assign(request, {
|
||||
responseMsToLive: hasValue(responseMsToLive) ? responseMsToLive : request.responseMsToLive,
|
||||
getResponseParser: getResponseParserFn
|
||||
getResponseParser: getResponseParserFn,
|
||||
searchOptions: searchOptions
|
||||
});
|
||||
}),
|
||||
configureRequest(this.requestService),
|
||||
);
|
||||
const requestEntryObs = requestObs.pipe(
|
||||
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||
return requestObs.pipe(
|
||||
switchMap((request:RestRequest) => this.requestService.getByHref(request.href)),
|
||||
map(((requestEntry:RequestEntry) => ({
|
||||
searchOptions: searchOptions,
|
||||
requestEntry: requestEntry
|
||||
})))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to convert the parsed responses into a paginated list of search results
|
||||
* @param searchEntries: The request entries from the search method
|
||||
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
||||
*/
|
||||
getPaginatedResults(searchEntries:Observable<{ searchOptions:PaginatedSearchOptions, requestEntry:RequestEntry }>)
|
||||
:Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||
const requestEntryObs:Observable<RequestEntry> = searchEntries.pipe(
|
||||
map((entry) => entry.requestEntry),
|
||||
);
|
||||
|
||||
// get search results from response cache
|
||||
const sqrObs: Observable<SearchQueryResponse> = requestEntryObs.pipe(
|
||||
const sqrObs:Observable<SearchQueryResponse> = requestEntryObs.pipe(
|
||||
filterSuccessfulResponses(),
|
||||
map((response: SearchSuccessResponse) => response.results)
|
||||
map((response:SearchSuccessResponse) => response.results),
|
||||
);
|
||||
|
||||
// turn dspace href from search results to effective list of DSpaceObjects
|
||||
@@ -187,11 +216,12 @@ export class SearchService implements OnDestroy {
|
||||
})
|
||||
);
|
||||
|
||||
return observableCombineLatest(hrefObs, tDomainListObs, requestEntryObs).pipe(
|
||||
switchMap(([href, tDomainList, requestEntry]) => {
|
||||
return observableCombineLatest(tDomainListObs, searchEntries).pipe(
|
||||
switchMap(([tDomainList, searchEntry]) => {
|
||||
const requestEntry = searchEntry.requestEntry;
|
||||
if (tDomainList.indexOf(undefined) > -1 && requestEntry && requestEntry.completed) {
|
||||
this.requestService.removeByHrefSubstring(href);
|
||||
return this.search(searchOptions)
|
||||
this.requestService.removeByHrefSubstring(requestEntry.request.href);
|
||||
return this.search(searchEntry.searchOptions)
|
||||
} else {
|
||||
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
|
||||
}
|
||||
|
@@ -1,24 +1,32 @@
|
||||
<ng-container *ngVar="(searchOptions$ | async) as config">
|
||||
<h3>{{ 'search.sidebar.settings.title' | translate}}</h3>
|
||||
<div *ngIf="config?.sort" class="setting-option result-order-settings mb-3 p-3">
|
||||
<h5>{{ 'search.sidebar.settings.sort-by' | translate}}</h5>
|
||||
<select class="form-control" (change)="reloadOrder($event)">
|
||||
<option *ngFor="let sortOption of searchOptionPossibilities"
|
||||
[value]="sortOption.field + ',' + sortOption.direction.toString()"
|
||||
[selected]="sortOption.field === config?.sort.field && sortOption.direction === (config?.sort.direction)? 'selected': null">
|
||||
{{'sorting.' + sortOption.field + '.' + sortOption.direction | translate}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<h3>{{ 'search.sidebar.settings.title' | translate}}</h3>
|
||||
|
||||
<div class="setting-option page-size-settings mb-3 p-3">
|
||||
<h5>{{ 'search.sidebar.settings.rpp' | translate}}</h5>
|
||||
<select class="form-control" (change)="reloadRPP($event)">
|
||||
<option *ngFor="let pageSizeOption of config?.pagination.pageSizeOptions"
|
||||
[value]="pageSizeOption"
|
||||
[selected]="pageSizeOption === +config?.pagination.pageSize ? 'selected': null">
|
||||
{{pageSizeOption}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="result-order-settings">
|
||||
<ds-sidebar-dropdown
|
||||
*ngIf="config?.sort"
|
||||
[id]="'search-sidebar-sort'"
|
||||
[label]="'search.sidebar.settings.sort-by'"
|
||||
(change)="reloadOrder($event)"
|
||||
>
|
||||
<option *ngFor="let sortOption of searchOptionPossibilities"
|
||||
[value]="sortOption.field + ',' + sortOption.direction.toString()"
|
||||
[selected]="sortOption.field === config?.sort.field && sortOption.direction === (config?.sort.direction)? 'selected': null">
|
||||
{{'sorting.' + sortOption.field + '.' + sortOption.direction | translate}}
|
||||
</option>
|
||||
</ds-sidebar-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="page-size-settings">
|
||||
<ds-sidebar-dropdown
|
||||
[id]="'search-sidebar-rpp'"
|
||||
[label]="'search.sidebar.settings.rpp'"
|
||||
(change)="reloadRPP($event)"
|
||||
>
|
||||
<option *ngFor="let pageSizeOption of config?.pagination.pageSizeOptions"
|
||||
[value]="pageSizeOption"
|
||||
[selected]="pageSizeOption === +config?.pagination.pageSize ? 'selected': null">
|
||||
{{pageSizeOption}}
|
||||
</option>
|
||||
</ds-sidebar-dropdown>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@@ -7,84 +7,92 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { SearchSidebarService } from '../search-sidebar/search-sidebar.service';
|
||||
import { SidebarService } from '../../shared/sidebar/sidebar.service';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { SearchFilterService } from '../search-filters/search-filter/search-filter.service';
|
||||
import { hot } from 'jasmine-marbles';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
|
||||
|
||||
describe('SearchSettingsComponent', () => {
|
||||
|
||||
let comp: SearchSettingsComponent;
|
||||
let fixture: ComponentFixture<SearchSettingsComponent>;
|
||||
let searchServiceObject: SearchService;
|
||||
let comp:SearchSettingsComponent;
|
||||
let fixture:ComponentFixture<SearchSettingsComponent>;
|
||||
let searchServiceObject:SearchService;
|
||||
|
||||
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
||||
pagination.id = 'search-results-pagination';
|
||||
pagination.currentPage = 1;
|
||||
pagination.pageSize = 10;
|
||||
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
||||
const mockResults = ['test', 'data'];
|
||||
const searchServiceStub = {
|
||||
searchOptions: { pagination: pagination, sort: sort },
|
||||
search: () => mockResults
|
||||
};
|
||||
let pagination:PaginationComponentOptions;
|
||||
let sort:SortOptions;
|
||||
let mockResults;
|
||||
let searchServiceStub;
|
||||
|
||||
const queryParam = 'test query';
|
||||
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||
const paginatedSearchOptions = {
|
||||
query: queryParam,
|
||||
scope: scopeParam,
|
||||
pagination,
|
||||
sort
|
||||
};
|
||||
let queryParam;
|
||||
let scopeParam;
|
||||
let paginatedSearchOptions;
|
||||
|
||||
const activatedRouteStub = {
|
||||
queryParams: observableOf({
|
||||
query: queryParam,
|
||||
scope: scopeParam
|
||||
})
|
||||
};
|
||||
let activatedRouteStub;
|
||||
|
||||
const sidebarService = {
|
||||
isCollapsed: observableOf(true),
|
||||
collapse: () => this.isCollapsed = observableOf(true),
|
||||
expand: () => this.isCollapsed = observableOf(false)
|
||||
};
|
||||
let sidebarService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
pagination = new PaginationComponentOptions();
|
||||
pagination.id = 'search-results-pagination';
|
||||
pagination.currentPage = 1;
|
||||
pagination.pageSize = 10;
|
||||
sort = new SortOptions('score', SortDirection.DESC);
|
||||
mockResults = ['test', 'data'];
|
||||
searchServiceStub = {
|
||||
searchOptions: {pagination: pagination, sort: sort},
|
||||
search: () => mockResults,
|
||||
};
|
||||
|
||||
queryParam = 'test query';
|
||||
scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||
paginatedSearchOptions = {
|
||||
query: queryParam,
|
||||
scope: scopeParam,
|
||||
pagination,
|
||||
sort,
|
||||
};
|
||||
|
||||
activatedRouteStub = {
|
||||
queryParams: observableOf({
|
||||
query: queryParam,
|
||||
scope: scopeParam,
|
||||
}),
|
||||
};
|
||||
|
||||
sidebarService = {
|
||||
isCollapsed: observableOf(true),
|
||||
collapse: () => this.isCollapsed = observableOf(true),
|
||||
expand: () => this.isCollapsed = observableOf(false),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
declarations: [SearchSettingsComponent, EnumKeysPipe, VarDirective],
|
||||
providers: [
|
||||
{ provide: SearchService, useValue: searchServiceStub },
|
||||
{provide: SearchService, useValue: searchServiceStub},
|
||||
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{provide: ActivatedRoute, useValue: activatedRouteStub},
|
||||
{
|
||||
provide: SearchSidebarService,
|
||||
useValue: sidebarService
|
||||
provide: SidebarService,
|
||||
useValue: sidebarService,
|
||||
},
|
||||
{
|
||||
provide: SearchFilterService,
|
||||
useValue: {}
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useValue: {
|
||||
paginatedSearchOptions: hot('a', {
|
||||
a: paginatedSearchOptions
|
||||
}),
|
||||
getCurrentScope: hot('a', {
|
||||
a: 'test-id'
|
||||
}),
|
||||
}
|
||||
paginatedSearchOptions: observableOf(paginatedSearchOptions),
|
||||
getCurrentScope: observableOf('test-id'),
|
||||
},
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
@@ -101,42 +109,46 @@ describe('SearchSettingsComponent', () => {
|
||||
|
||||
});
|
||||
|
||||
it('it should show the order settings with the respective selectable options', () => {
|
||||
(comp as any).searchOptions$.pipe(first()).subscribe((options) => {
|
||||
it('it should show the order settings with the respective selectable options', (done) => {
|
||||
(comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
|
||||
fixture.detectChanges();
|
||||
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
||||
expect(orderSetting).toBeDefined();
|
||||
const childElements = orderSetting.query(By.css('.form-control')).children;
|
||||
const childElements = orderSetting.queryAll(By.css('option'));
|
||||
expect(childElements.length).toEqual(comp.searchOptionPossibilities.length);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('it should show the size settings with the respective selectable options', () => {
|
||||
(comp as any).searchOptions$.pipe(first()).subscribe((options) => {
|
||||
it('it should show the size settings with the respective selectable options', (done) => {
|
||||
(comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
|
||||
fixture.detectChanges();
|
||||
const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings'));
|
||||
expect(pageSizeSetting).toBeDefined();
|
||||
const childElements = pageSizeSetting.query(By.css('.form-control')).children;
|
||||
const childElements = pageSizeSetting.queryAll(By.css('option'));
|
||||
expect(childElements.length).toEqual(options.pagination.pageSizeOptions.length);
|
||||
}
|
||||
)
|
||||
done();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should have the proper order value selected by default', () => {
|
||||
(comp as any).searchOptions$.pipe(first()).subscribe((options) => {
|
||||
it('should have the proper order value selected by default', (done) => {
|
||||
(comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
|
||||
fixture.detectChanges();
|
||||
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
||||
const childElementToBeSelected = orderSetting.query(By.css('.form-control option[value="0"][selected="selected"]'));
|
||||
const childElementToBeSelected = orderSetting.query(By.css('option[value="0"][selected="selected"]'));
|
||||
expect(childElementToBeSelected).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have the proper rpp value selected by default', () => {
|
||||
(comp as any).searchOptions$.pipe(first()).subscribe((options) => {
|
||||
it('should have the proper rpp value selected by default', (done) => {
|
||||
(comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
|
||||
fixture.detectChanges();
|
||||
const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings'));
|
||||
const childElementToBeSelected = pageSizeSetting.query(By.css('.form-control option[value="10"][selected="selected"]'));
|
||||
const childElementToBeSelected = pageSizeSetting.query(By.css('option[value="10"][selected="selected"]'));
|
||||
expect(childElementToBeSelected).toBeDefined();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -1,47 +0,0 @@
|
||||
import { SearchSidebarAction, SearchSidebarActionTypes } from './search-sidebar.actions';
|
||||
|
||||
/**
|
||||
* Interface that represents the state of the sidebar
|
||||
*/
|
||||
export interface SearchSidebarState {
|
||||
sidebarCollapsed: boolean;
|
||||
}
|
||||
|
||||
const initialState: SearchSidebarState = {
|
||||
sidebarCollapsed: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs a search sidebar action on the current state
|
||||
* @param {SearchSidebarState} state The state before the action is performed
|
||||
* @param {SearchSidebarAction} action The action that should be performed
|
||||
* @returns {SearchSidebarState} The state after the action is performed
|
||||
*/
|
||||
export function sidebarReducer(state = initialState, action: SearchSidebarAction): SearchSidebarState {
|
||||
switch (action.type) {
|
||||
|
||||
case SearchSidebarActionTypes.COLLAPSE: {
|
||||
return Object.assign({}, state, {
|
||||
sidebarCollapsed: true
|
||||
});
|
||||
}
|
||||
|
||||
case SearchSidebarActionTypes.EXPAND: {
|
||||
return Object.assign({}, state, {
|
||||
sidebarCollapsed: false
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
case SearchSidebarActionTypes.TOGGLE: {
|
||||
return Object.assign({}, state, {
|
||||
sidebarCollapsed: !state.sidebarCollapsed
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
1
src/app/+search-page/search-tracker.component.html
Normal file
1
src/app/+search-page/search-tracker.component.html
Normal file
@@ -0,0 +1 @@
|
||||
|
3
src/app/+search-page/search-tracker.component.scss
Normal file
3
src/app/+search-page/search-tracker.component.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: none
|
||||
}
|
88
src/app/+search-page/search-tracker.component.ts
Normal file
88
src/app/+search-page/search-tracker.component.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { filter, map, switchMap } from 'rxjs/operators';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||
import { RouteService } from '../core/services/route.service';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { SearchQueryResponse } from './search-service/search-query-response.model';
|
||||
import { SearchSuccessResponse } from '../core/cache/response.models';
|
||||
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||
|
||||
/**
|
||||
* This component triggers a page view statistic
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-search-tracker',
|
||||
styleUrls: ['./search-tracker.component.scss'],
|
||||
templateUrl: './search-tracker.component.html',
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
export class SearchTrackerComponent extends SearchComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
protected service:SearchService,
|
||||
protected sidebarService:SidebarService,
|
||||
protected windowService:HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService:SearchConfigurationService,
|
||||
protected routeService:RouteService,
|
||||
public angulartics2:Angulartics2
|
||||
) {
|
||||
super(service, sidebarService, windowService, searchConfigService, routeService);
|
||||
}
|
||||
|
||||
ngOnInit():void {
|
||||
// super.ngOnInit();
|
||||
this.getSearchOptions().pipe(
|
||||
switchMap((options) => this.service.searchEntries(options)
|
||||
.pipe(
|
||||
filter((entry) =>
|
||||
hasValue(entry.requestEntry)
|
||||
&& hasValue(entry.requestEntry.response)
|
||||
&& entry.requestEntry.response.isSuccessful === true
|
||||
),
|
||||
map((entry) => ({
|
||||
searchOptions: entry.searchOptions,
|
||||
response: (entry.requestEntry.response as SearchSuccessResponse).results
|
||||
})),
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe((entry) => {
|
||||
const config:PaginatedSearchOptions = entry.searchOptions;
|
||||
const searchQueryResponse:SearchQueryResponse = entry.response;
|
||||
const filters:Array<{ filter:string, operator:string, value:string, label:string; }> = [];
|
||||
const appliedFilters = searchQueryResponse.appliedFilters || [];
|
||||
for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) {
|
||||
const appliedFilter = appliedFilters[i];
|
||||
filters.push(appliedFilter);
|
||||
}
|
||||
this.angulartics2.eventTrack.next({
|
||||
action: 'search',
|
||||
properties: {
|
||||
searchOptions: config,
|
||||
page: {
|
||||
size: config.pagination.size, // same as searchQueryResponse.page.elementsPerPage
|
||||
totalElements: searchQueryResponse.page.totalElements,
|
||||
totalPages: searchQueryResponse.page.totalPages,
|
||||
number: config.pagination.currentPage, // same as searchQueryResponse.page.currentPage
|
||||
},
|
||||
sort: {
|
||||
by: config.sort.field,
|
||||
order: config.sort.direction
|
||||
},
|
||||
filters: filters,
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
50
src/app/+search-page/search.component.html
Normal file
50
src/app/+search-page/search.component.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<div class="container" *ngIf="(isXsOrSm$ | async)">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ds-page-with-sidebar [id]="'search-page'" [sidebarContent]="sidebarContent">
|
||||
<div class="row">
|
||||
<div class="col-12" *ngIf="!(isXsOrSm$ | async)">
|
||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||
</div>
|
||||
<div id="search-content" class="col-12">
|
||||
<div class="d-block d-md-none search-controls clearfix">
|
||||
<ds-view-mode-switch [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
|
||||
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
||||
| translate}}
|
||||
</button>
|
||||
</div>
|
||||
<ds-search-results [searchResults]="resultsRD$ | async"
|
||||
[searchConfig]="searchOptions$ | async"
|
||||
[configuration]="configuration$ | async"
|
||||
[disableHeader]="!searchEnabled"></ds-search-results>
|
||||
</div>
|
||||
</div>
|
||||
</ds-page-with-sidebar>
|
||||
|
||||
<ng-template #sidebarContent>
|
||||
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
(toggleSidebar)="closeSidebar()"
|
||||
>
|
||||
</ds-search-sidebar>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #searchForm>
|
||||
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
||||
[query]="(searchOptions$ | async)?.query"
|
||||
[scope]="(searchOptions$ | async)?.scope"
|
||||
[currentUrl]="searchLink"
|
||||
[scopes]="(scopeListRD$ | async)"
|
||||
[inPlaceSearch]="inPlaceSearch">
|
||||
</ds-search-form>
|
||||
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||
</ng-template>
|
10
src/app/+search-page/search.component.scss
Normal file
10
src/app/+search-page/search.component.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
@include media-breakpoint-down(md) {
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .search-controls {
|
||||
margin-bottom: $spacer;
|
||||
}
|
@@ -10,13 +10,13 @@ import { SortDirection, SortOptions } from '../core/cache/models/sort-options.mo
|
||||
import { CommunityDataService } from '../core/data/community-data.service';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
@@ -27,11 +27,11 @@ import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||
import { SearchFixedFilterService } from './search-filters/search-filter/search-fixed-filter.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../shared/testing/utils';
|
||||
|
||||
let comp: SearchPageComponent;
|
||||
let fixture: ComponentFixture<SearchPageComponent>;
|
||||
let comp: SearchComponent;
|
||||
let fixture: ComponentFixture<SearchComponent>;
|
||||
let searchServiceObject: SearchService;
|
||||
let searchConfigurationServiceObject: SearchConfigurationService;
|
||||
const store: Store<SearchPageComponent> = jasmine.createSpyObj('store', {
|
||||
const store: Store<SearchComponent> = jasmine.createSpyObj('store', {
|
||||
/* tslint:disable:no-empty */
|
||||
dispatch: {},
|
||||
/* tslint:enable:no-empty */
|
||||
@@ -115,7 +115,7 @@ export function configureSearchComponentTestingModule(compType) {
|
||||
})
|
||||
},
|
||||
{
|
||||
provide: SearchSidebarService,
|
||||
provide: SidebarService,
|
||||
useValue: sidebarService
|
||||
},
|
||||
{
|
||||
@@ -150,14 +150,14 @@ export function configureSearchComponentTestingModule(compType) {
|
||||
}).compileComponents();
|
||||
}
|
||||
|
||||
describe('SearchPageComponent', () => {
|
||||
describe('SearchComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
configureSearchComponentTestingModule(SearchPageComponent);
|
||||
configureSearchComponentTestingModule(SearchComponent);
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchPageComponent);
|
||||
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||
fixture = TestBed.createComponent(SearchComponent);
|
||||
comp = fixture.componentInstance; // SearchComponent test instance
|
||||
fixture.detectChanges();
|
||||
searchServiceObject = (comp as any).service;
|
||||
searchConfigurationServiceObject = (comp as any).searchConfigService;
|
||||
@@ -191,34 +191,4 @@ describe('SearchPageComponent', () => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when sidebarCollapsed is true in mobile view', () => {
|
||||
let menu: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement;
|
||||
(comp as any).isSidebarCollapsed$ = observableOf(true);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should close the sidebar', () => {
|
||||
expect(menu.classList).not.toContain('active');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when sidebarCollapsed is false in mobile view', () => {
|
||||
let menu: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement;
|
||||
(comp as any).isSidebarCollapsed$ = observableOf(false);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should open the menu', () => {
|
||||
expect(menu.classList).toContain('active');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
175
src/app/+search-page/search.component.ts
Normal file
175
src/app/+search-page/search.component.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { startWith, switchMap, } from 'rxjs/operators';
|
||||
import { PaginatedList } from '../core/data/paginated-list';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||
import { pushInOut } from '../shared/animations/push';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { PaginatedSearchOptions } from './paginated-search-options.model';
|
||||
import { SearchResult } from './search-result.model';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { SearchConfigurationService } from './search-service/search-configuration.service';
|
||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
import { RouteService } from '../core/services/route.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search',
|
||||
styleUrls: ['./search.component.scss'],
|
||||
templateUrl: './search.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [pushInOut],
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: SearchConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* This component renders a sidebar, a search input bar and the search results.
|
||||
*/
|
||||
export class SearchComponent implements OnInit {
|
||||
/**
|
||||
* The current search results
|
||||
*/
|
||||
resultsRD$: BehaviorSubject<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> = new BehaviorSubject(null);
|
||||
|
||||
/**
|
||||
* The current paginated search options
|
||||
*/
|
||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||
|
||||
/**
|
||||
* The current relevant scopes
|
||||
*/
|
||||
scopeListRD$: Observable<DSpaceObject[]>;
|
||||
|
||||
/**
|
||||
* Emits true if were on a small screen
|
||||
*/
|
||||
isXsOrSm$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Subscription to unsubscribe from
|
||||
*/
|
||||
sub: Subscription;
|
||||
|
||||
/**
|
||||
* True when the search component should show results on the current page
|
||||
*/
|
||||
@Input() inPlaceSearch = true;
|
||||
|
||||
/**
|
||||
* Whether or not the search bar should be visible
|
||||
*/
|
||||
@Input()
|
||||
searchEnabled = true;
|
||||
|
||||
/**
|
||||
* The width of the sidebar (bootstrap columns)
|
||||
*/
|
||||
@Input()
|
||||
sideBarWidth = 3;
|
||||
|
||||
/**
|
||||
* The currently applied configuration (determines title of search)
|
||||
*/
|
||||
@Input()
|
||||
configuration$: Observable<string>;
|
||||
|
||||
/**
|
||||
* Link to the search page
|
||||
*/
|
||||
searchLink: string;
|
||||
|
||||
/**
|
||||
* Observable for whether or not the sidebar is currently collapsed
|
||||
*/
|
||||
isSidebarCollapsed$: Observable<boolean>;
|
||||
|
||||
constructor(protected service: SearchService,
|
||||
protected sidebarService: SidebarService,
|
||||
protected windowService: HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||
protected routeService: RouteService) {
|
||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listening to changes in the paginated search options
|
||||
* If something changes, update the search results
|
||||
*
|
||||
* Listen to changes in the scope
|
||||
* If something changes, update the list of scopes for the dropdown
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.isSidebarCollapsed$ = this.isSidebarCollapsed();
|
||||
this.searchLink = this.getSearchLink();
|
||||
this.searchOptions$ = this.getSearchOptions();
|
||||
this.sub = this.searchOptions$.pipe(
|
||||
switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData(), startWith(undefined))))
|
||||
.subscribe((results) => {
|
||||
this.resultsRD$.next(results);
|
||||
});
|
||||
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||
switchMap((scopeId) => this.service.getScopes(scopeId))
|
||||
);
|
||||
if (!isNotEmpty(this.configuration$)) {
|
||||
this.configuration$ = this.routeService.getRouteParameterValue('configuration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current paginated search options
|
||||
* @returns {Observable<PaginatedSearchOptions>}
|
||||
*/
|
||||
protected getSearchOptions(): Observable<PaginatedSearchOptions> {
|
||||
return this.searchConfigService.paginatedSearchOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to a collapsed state
|
||||
*/
|
||||
public closeSidebar(): void {
|
||||
this.sidebarService.collapse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to an expanded state
|
||||
*/
|
||||
public openSidebar(): void {
|
||||
this.sidebarService.expand();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the sidebar is collapsed
|
||||
* @returns {Observable<boolean>} emits true if the sidebar is currently collapsed, false if it is expanded
|
||||
*/
|
||||
private isSidebarCollapsed(): Observable<boolean> {
|
||||
return this.sidebarService.isCollapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
|
||||
*/
|
||||
private getSearchLink(): string {
|
||||
if (this.inPlaceSearch) {
|
||||
return './';
|
||||
}
|
||||
return this.service.getSearchLink();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from the subscription
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.sub)) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
@@ -46,6 +46,7 @@ import { MockActivatedRoute } from './shared/mocks/mock-active-router';
|
||||
import { MockRouter } from './shared/mocks/mock-router';
|
||||
import { MockCookieService } from './shared/mocks/mock-cookie.service';
|
||||
import { CookieService } from './core/services/cookie.service';
|
||||
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
||||
|
||||
let comp: AppComponent;
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
@@ -74,6 +75,7 @@ describe('App component', () => {
|
||||
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
|
||||
{ provide: MetadataService, useValue: new MockMetadataService() },
|
||||
{ provide: Angulartics2GoogleAnalytics, useValue: new AngularticsMock() },
|
||||
{ provide: Angulartics2DSpace, useValue: new AngularticsMock() },
|
||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||
{ provide: Router, useValue: new MockRouter() },
|
||||
{ provide: ActivatedRoute, useValue: new MockActivatedRoute() },
|
||||
|
@@ -34,6 +34,7 @@ import { HostWindowService } from './shared/host-window.service';
|
||||
import { Theme } from '../config/theme.inferface';
|
||||
import { isNotEmpty } from './shared/empty.util';
|
||||
import { CookieService } from './core/services/cookie.service';
|
||||
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
|
||||
|
||||
export const LANG_COOKIE = 'language_cookie';
|
||||
|
||||
@@ -60,6 +61,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
private store: Store<HostWindowState>,
|
||||
private metadata: MetadataService,
|
||||
private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics,
|
||||
private angulartics2DSpace: Angulartics2DSpace,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private cssService: CSSVariableService,
|
||||
@@ -89,6 +91,8 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
angulartics2DSpace.startTracking();
|
||||
|
||||
metadata.listenForRouteChange();
|
||||
|
||||
if (config.debug) {
|
||||
|
@@ -1,12 +1,16 @@
|
||||
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
|
||||
import { ActionReducerMap, createSelector, MemoizedSelector, State } from '@ngrx/store';
|
||||
import * as fromRouter from '@ngrx/router-store';
|
||||
import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer';
|
||||
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
|
||||
import { formReducer, FormState } from './shared/form/form.reducer';
|
||||
import {
|
||||
SearchSidebarState,
|
||||
SidebarState,
|
||||
sidebarReducer
|
||||
} from './+search-page/search-sidebar/search-sidebar.reducer';
|
||||
} from './shared/sidebar/sidebar.reducer';
|
||||
import {
|
||||
SidebarFilterState,
|
||||
sidebarFilterReducer, SidebarFiltersState
|
||||
} from './shared/sidebar/filter/sidebar-filter.reducer';
|
||||
import {
|
||||
filterReducer,
|
||||
SearchFiltersState
|
||||
@@ -38,7 +42,8 @@ export interface AppState {
|
||||
metadataRegistry: MetadataRegistryState;
|
||||
bitstreamFormats: BitstreamFormatRegistryState;
|
||||
notifications: NotificationsState;
|
||||
searchSidebar: SearchSidebarState;
|
||||
sidebar: SidebarState;
|
||||
sidebarFilter: SidebarFiltersState;
|
||||
searchFilter: SearchFiltersState;
|
||||
truncatable: TruncatablesState;
|
||||
cssVariables: CSSVariablesState;
|
||||
@@ -55,7 +60,8 @@ export const appReducers: ActionReducerMap<AppState> = {
|
||||
metadataRegistry: metadataRegistryReducer,
|
||||
bitstreamFormats: bitstreamFormatReducer,
|
||||
notifications: notificationsReducer,
|
||||
searchSidebar: sidebarReducer,
|
||||
sidebar: sidebarReducer,
|
||||
sidebarFilter: sidebarFilterReducer,
|
||||
searchFilter: filterReducer,
|
||||
truncatable: truncatableReducer,
|
||||
cssVariables: cssVariablesReducer,
|
||||
|
13
src/app/core/cache/models/normalized-site.model.ts
vendored
Normal file
13
src/app/core/cache/models/normalized-site.model.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import { inheritSerialization } from 'cerialize';
|
||||
import { NormalizedDSpaceObject } from './normalized-dspace-object.model';
|
||||
import { mapsTo } from '../builders/build-decorators';
|
||||
import { Site } from '../../shared/site.model';
|
||||
|
||||
/**
|
||||
* Normalized model class for a Site object
|
||||
*/
|
||||
@mapsTo(Site)
|
||||
@inheritSerialization(NormalizedDSpaceObject)
|
||||
export class NormalizedSite extends NormalizedDSpaceObject<Site> {
|
||||
|
||||
}
|
@@ -121,6 +121,8 @@ import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model';
|
||||
import { BrowseDefinition } from './shared/browse-definition.model';
|
||||
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
|
||||
import { ObjectSelectService } from '../shared/object-select/object-select.service';
|
||||
import { SiteDataService } from './data/site-data.service';
|
||||
import { NormalizedSite } from './cache/models/normalized-site.model';
|
||||
|
||||
const IMPORTS = [
|
||||
CommonModule,
|
||||
@@ -139,6 +141,7 @@ const PROVIDERS = [
|
||||
AuthResponseParsingService,
|
||||
CommunityDataService,
|
||||
CollectionDataService,
|
||||
SiteDataService,
|
||||
DSOResponseParsingService,
|
||||
DSpaceRESTv2Service,
|
||||
DynamicFormLayoutService,
|
||||
@@ -232,6 +235,7 @@ export const normalizedModels =
|
||||
NormalizedBitstream,
|
||||
NormalizedBitstreamFormat,
|
||||
NormalizedItem,
|
||||
NormalizedSite,
|
||||
NormalizedCollection,
|
||||
NormalizedCommunity,
|
||||
NormalizedEPerson,
|
||||
|
@@ -22,6 +22,10 @@ export class SearchResponseParsingService implements ResponseParsingService {
|
||||
}
|
||||
};
|
||||
const payload = data.payload._embedded.searchResult || emptyPayload;
|
||||
payload.appliedFilters = data.payload.appliedFilters;
|
||||
payload.sort = data.payload.sort;
|
||||
payload.scope = data.payload.scope;
|
||||
payload.configuration = data.payload.configuration;
|
||||
const hitHighlights: MetadataMap[] = payload._embedded.objects
|
||||
.map((object) => object.hitHighlights)
|
||||
.map((hhObject) => {
|
||||
|
104
src/app/core/data/site-data.service.spec.ts
Normal file
104
src/app/core/data/site-data.service.spec.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||
import { SiteDataService } from './site-data.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { Site } from '../shared/site.model';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { RequestEntry } from './request.reducer';
|
||||
import { FindAllOptions } from './request.models';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { RemoteData } from './remote-data';
|
||||
|
||||
describe('SiteDataService', () => {
|
||||
let scheduler:TestScheduler;
|
||||
let service:SiteDataService;
|
||||
let halService:HALEndpointService;
|
||||
let requestService:RequestService;
|
||||
let rdbService:RemoteDataBuildService;
|
||||
let objectCache:ObjectCacheService;
|
||||
|
||||
const testObject = Object.assign(new Site(), {
|
||||
uuid: '9b4f22f4-164a-49db-8817-3316b6ee5746',
|
||||
});
|
||||
|
||||
const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2';
|
||||
const options = Object.assign(new FindAllOptions(), {});
|
||||
|
||||
const getRequestEntry$ = (successful:boolean, statusCode:number, statusText:string) => {
|
||||
return observableOf({
|
||||
response: new RestResponse(successful, statusCode, statusText)
|
||||
} as RequestEntry);
|
||||
};
|
||||
|
||||
const siteLink = 'https://rest.api/rest/api/config/sites';
|
||||
|
||||
beforeEach(() => {
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: cold('a', {a: siteLink})
|
||||
});
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
generateRequestId: requestUUID,
|
||||
configure: true,
|
||||
getByHref: getRequestEntry$(true, 200, 'Success')
|
||||
});
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildList: cold('a', {
|
||||
a: new RemoteData(false, false, true, undefined, new PaginatedList(null, [testObject]))
|
||||
})
|
||||
});
|
||||
|
||||
const store = {} as Store<CoreState>;
|
||||
objectCache = {} as ObjectCacheService;
|
||||
const notificationsService = {} as NotificationsService;
|
||||
const http = {} as HttpClient;
|
||||
const comparator = {} as any;
|
||||
const dataBuildService = {} as NormalizedObjectBuildService;
|
||||
|
||||
service = new SiteDataService(
|
||||
requestService,
|
||||
rdbService,
|
||||
dataBuildService,
|
||||
store,
|
||||
objectCache,
|
||||
halService,
|
||||
notificationsService,
|
||||
http,
|
||||
comparator
|
||||
);
|
||||
});
|
||||
|
||||
describe('getBrowseEndpoint', () => {
|
||||
it('should return the Static Page endpoint', () => {
|
||||
|
||||
const result = service.getBrowseEndpoint(options);
|
||||
const expected = cold('b', {b: siteLink});
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('find', () => {
|
||||
it('should return the Site object', () => {
|
||||
|
||||
spyOn(service, 'findAll').and.returnValue(cold('a', {
|
||||
a: new RemoteData(false, false, true, undefined, new PaginatedList(null, [testObject]))
|
||||
}));
|
||||
|
||||
const expected = cold('(b|)', {b: testObject});
|
||||
const result = service.find();
|
||||
|
||||
expect(result).toBeObservable(expected);
|
||||
});
|
||||
});
|
||||
});
|
68
src/app/core/data/site-data.service.ts
Normal file
68
src/app/core/data/site-data.service.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { DataService } from './data.service';
|
||||
import { Site } from '../shared/site.model';
|
||||
import { RequestService } from './request.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||
import { FindAllOptions } from './request.models';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { getSucceededRemoteData } from '../shared/operators';
|
||||
|
||||
/**
|
||||
* Service responsible for handling requests related to the Site object
|
||||
*/
|
||||
@Injectable()
|
||||
export class SiteDataService extends DataService<Site> {
|
||||
|
||||
protected linkPath = 'sites';
|
||||
protected forceBypassCache = false;
|
||||
|
||||
|
||||
constructor(
|
||||
protected requestService:RequestService,
|
||||
protected rdbService:RemoteDataBuildService,
|
||||
protected dataBuildService:NormalizedObjectBuildService,
|
||||
protected store:Store<CoreState>,
|
||||
protected objectCache:ObjectCacheService,
|
||||
protected halService:HALEndpointService,
|
||||
protected notificationsService:NotificationsService,
|
||||
protected http:HttpClient,
|
||||
protected comparator:DSOChangeAnalyzer<Site>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the endpoint for browsing the site object
|
||||
* @param {FindAllOptions} options
|
||||
* @param {Observable<string>} linkPath
|
||||
*/
|
||||
getBrowseEndpoint(options:FindAllOptions, linkPath?:string):Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the Site Object
|
||||
*/
|
||||
find():Observable<Site> {
|
||||
return this.findAll().pipe(
|
||||
getSucceededRemoteData(),
|
||||
map((remoteData:RemoteData<PaginatedList<Site>>) => remoteData.payload),
|
||||
map((list:PaginatedList<Site>) => list.page[0])
|
||||
);
|
||||
}
|
||||
}
|
11
src/app/core/shared/site.model.ts
Normal file
11
src/app/core/shared/site.model.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { DSpaceObject } from './dspace-object.model';
|
||||
import { ResourceType } from './resource-type';
|
||||
|
||||
/**
|
||||
* Model class for the Site object
|
||||
*/
|
||||
export class Site extends DSpaceObject {
|
||||
|
||||
static type = new ResourceType('site');
|
||||
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { SubmissionService } from '../../submission/submission.service';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { SubmissionObject } from './models/submission-object.model';
|
||||
import { WorkspaceItem } from './models/workspaceitem.model';
|
||||
import { SubmissionObjectDataService } from './submission-object-data.service';
|
||||
import { SubmissionScopeType } from './submission-scope-type';
|
||||
import { WorkflowItemDataService } from './workflowitem-data.service';
|
||||
import { WorkspaceitemDataService } from './workspaceitem-data.service';
|
||||
|
||||
describe('SubmissionObjectDataService', () => {
|
||||
let service: SubmissionObjectDataService;
|
||||
let submissionService: SubmissionService;
|
||||
let workspaceitemDataService: WorkspaceitemDataService;
|
||||
let workflowItemDataService: WorkflowItemDataService;
|
||||
|
||||
const submissionId = '1234';
|
||||
const wsiResult = 'wsiResult' as any;
|
||||
const wfiResult = 'wfiResult' as any;
|
||||
|
||||
beforeEach(() => {
|
||||
workspaceitemDataService = jasmine.createSpyObj('WorkspaceitemDataService', {
|
||||
findById: wsiResult
|
||||
});
|
||||
workflowItemDataService = jasmine.createSpyObj('WorkflowItemDataService', {
|
||||
findById: wfiResult
|
||||
});
|
||||
});
|
||||
|
||||
describe('findById', () => {
|
||||
it('should call SubmissionService.getSubmissionScope to determine the type of submission object', () => {
|
||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||
getSubmissionScope: {}
|
||||
});
|
||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService);
|
||||
service.findById(submissionId);
|
||||
expect(submissionService.getSubmissionScope).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when the submission ID refers to a WorkspaceItem', () => {
|
||||
beforeEach(() => {
|
||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||
getSubmissionScope: SubmissionScopeType.WorkspaceItem
|
||||
});
|
||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService);
|
||||
});
|
||||
|
||||
it('should forward the result of WorkspaceitemDataService.findById()', () => {
|
||||
const result = service.findById(submissionId);
|
||||
expect(workspaceitemDataService.findById).toHaveBeenCalledWith(submissionId);
|
||||
expect(result).toBe(wsiResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the submission ID refers to a WorkflowItem', () => {
|
||||
beforeEach(() => {
|
||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||
getSubmissionScope: SubmissionScopeType.WorkflowItem
|
||||
});
|
||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService);
|
||||
});
|
||||
|
||||
it('should forward the result of WorkflowItemDataService.findById()', () => {
|
||||
const result = service.findById(submissionId);
|
||||
expect(workflowItemDataService.findById).toHaveBeenCalledWith(submissionId);
|
||||
expect(result).toBe(wfiResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the type of submission object is unknown', () => {
|
||||
beforeEach(() => {
|
||||
submissionService = jasmine.createSpyObj('SubmissionService', {
|
||||
getSubmissionScope: 'Something else'
|
||||
});
|
||||
service = new SubmissionObjectDataService(workspaceitemDataService, workflowItemDataService, submissionService);
|
||||
});
|
||||
|
||||
it('shouldn\'t call any data service methods', () => {
|
||||
service.findById(submissionId);
|
||||
expect(workspaceitemDataService.findById).not.toHaveBeenCalled();
|
||||
expect(workflowItemDataService.findById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return a RemoteData containing an error', (done) => {
|
||||
const result = service.findById(submissionId);
|
||||
result.subscribe((rd: RemoteData<SubmissionObject>) => {
|
||||
expect(rd.hasFailed).toBe(true);
|
||||
expect(rd.error).toBeDefined();
|
||||
done();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
46
src/app/core/submission/submission-object-data.service.ts
Normal file
46
src/app/core/submission/submission-object-data.service.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { of as observableOf, Observable } from 'rxjs';
|
||||
import { SubmissionService } from '../../submission/submission.service';
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { RemoteDataError } from '../data/remote-data-error';
|
||||
import { SubmissionObject } from './models/submission-object.model';
|
||||
import { SubmissionScopeType } from './submission-scope-type';
|
||||
import { WorkflowItemDataService } from './workflowitem-data.service';
|
||||
import { WorkspaceitemDataService } from './workspaceitem-data.service';
|
||||
|
||||
/**
|
||||
* A service to retrieve submission objects (WorkspaceItem/WorkflowItem)
|
||||
* without knowing their type
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SubmissionObjectDataService {
|
||||
constructor(
|
||||
private workspaceitemDataService: WorkspaceitemDataService,
|
||||
private workflowItemDataService: WorkflowItemDataService,
|
||||
private submissionService: SubmissionService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a submission object based on its ID.
|
||||
*
|
||||
* @param id The identifier of a submission object
|
||||
*/
|
||||
findById(id: string): Observable<RemoteData<SubmissionObject>> {
|
||||
switch (this.submissionService.getSubmissionScope()) {
|
||||
case SubmissionScopeType.WorkspaceItem:
|
||||
return this.workspaceitemDataService.findById(id);
|
||||
case SubmissionScopeType.WorkflowItem:
|
||||
return this.workflowItemDataService.findById(id);
|
||||
default:
|
||||
const error = new RemoteDataError(
|
||||
undefined,
|
||||
undefined,
|
||||
'The request couldn\'t be sent. Unable to determine the type of submission object'
|
||||
);
|
||||
return observableOf(new RemoteData(false, false, false, error, undefined));
|
||||
}
|
||||
}
|
||||
}
|
@@ -112,6 +112,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
|
||||
repeatable: false
|
||||
}),
|
||||
new DynamicRelationGroupModel({
|
||||
submissionId: '1234',
|
||||
id: 'relationGroup',
|
||||
formConfiguration: [],
|
||||
mandatoryField: '',
|
||||
|
@@ -33,6 +33,8 @@ export let FORM_GROUP_TEST_GROUP;
|
||||
|
||||
const config: GlobalConfig = MOCK_SUBMISSION_CONFIG;
|
||||
|
||||
const submissionId = '1234';
|
||||
|
||||
function init() {
|
||||
FORM_GROUP_TEST_MODEL_CONFIG = {
|
||||
disabled: false,
|
||||
@@ -67,6 +69,7 @@ function init() {
|
||||
}]
|
||||
} as FormFieldModel]
|
||||
} as FormRowModel],
|
||||
submissionId,
|
||||
id: 'dc_contributor_author',
|
||||
label: 'Authors',
|
||||
mandatoryField: 'dc.contributor.author',
|
||||
@@ -183,7 +186,7 @@ describe('DsDynamicRelationGroupComponent test suite', () => {
|
||||
|
||||
it('should init component properly', inject([FormBuilderService], (service: FormBuilderService) => {
|
||||
const formConfig = { rows: groupComp.model.formConfiguration } as SubmissionFormsModel;
|
||||
const formModel = service.modelFromConfiguration(formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly);
|
||||
const formModel = service.modelFromConfiguration(submissionId, formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly);
|
||||
const chips = new Chips([], 'value', 'dc.contributor.author');
|
||||
groupComp.formCollapsed.subscribe((value) => {
|
||||
expect(value).toEqual(false);
|
||||
@@ -257,7 +260,7 @@ describe('DsDynamicRelationGroupComponent test suite', () => {
|
||||
|
||||
it('should init component properly', inject([FormBuilderService], (service: FormBuilderService) => {
|
||||
const formConfig = { rows: groupComp.model.formConfiguration } as SubmissionFormsModel;
|
||||
const formModel = service.modelFromConfiguration(formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly);
|
||||
const formModel = service.modelFromConfiguration(submissionId, formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly);
|
||||
const chips = new Chips(modelValue, 'value', 'dc.contributor.author');
|
||||
groupComp.formCollapsed.subscribe((value) => {
|
||||
expect(value).toEqual(true);
|
||||
|
@@ -93,6 +93,7 @@ export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent
|
||||
|
||||
this.formId = this.formService.getUniqueId(this.model.id);
|
||||
this.formModel = this.formBuilderService.modelFromConfiguration(
|
||||
this.model.submissionId,
|
||||
config,
|
||||
this.model.scopeUUID,
|
||||
{},
|
||||
|
@@ -10,6 +10,7 @@ export const PLACEHOLDER_PARENT_METADATA = '#PLACEHOLDER_PARENT_METADATA_VALUE#'
|
||||
* Dynamic Group Model configuration interface
|
||||
*/
|
||||
export interface DynamicRelationGroupModelConfig extends DsDynamicInputModelConfig {
|
||||
submissionId: string,
|
||||
formConfiguration: FormRowModel[],
|
||||
mandatoryField: string,
|
||||
relationFields: string[],
|
||||
@@ -21,6 +22,7 @@ export interface DynamicRelationGroupModelConfig extends DsDynamicInputModelConf
|
||||
* Dynamic Group Model class
|
||||
*/
|
||||
export class DynamicRelationGroupModel extends DsDynamicInputModel {
|
||||
@serializable() submissionId: string;
|
||||
@serializable() formConfiguration: FormRowModel[];
|
||||
@serializable() mandatoryField: string;
|
||||
@serializable() relationFields: string[];
|
||||
@@ -32,6 +34,7 @@ export class DynamicRelationGroupModel extends DsDynamicInputModel {
|
||||
constructor(config: DynamicRelationGroupModelConfig, layout?: DynamicFormControlLayout) {
|
||||
super(config, layout);
|
||||
|
||||
this.submissionId = config.submissionId;
|
||||
this.formConfiguration = config.formConfiguration;
|
||||
this.mandatoryField = config.mandatoryField;
|
||||
this.relationFields = config.relationFields;
|
||||
|
@@ -56,6 +56,8 @@ describe('FormBuilderService test suite', () => {
|
||||
let testFormConfiguration: SubmissionFormsModel;
|
||||
let service: FormBuilderService;
|
||||
|
||||
const submissionId = '1234';
|
||||
|
||||
function testValidator() {
|
||||
return {testValidator: {valid: true}};
|
||||
}
|
||||
@@ -204,6 +206,7 @@ describe('FormBuilderService test suite', () => {
|
||||
new DynamicListRadioGroupModel({id: 'testRadioList', authorityOptions: authorityOptions, repeatable: false}),
|
||||
|
||||
new DynamicRelationGroupModel({
|
||||
submissionId,
|
||||
id: 'testRelationGroup',
|
||||
formConfiguration: [{
|
||||
fields: [{
|
||||
@@ -406,7 +409,7 @@ describe('FormBuilderService test suite', () => {
|
||||
});
|
||||
|
||||
it('should create an array of form models', () => {
|
||||
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
|
||||
const formModel = service.modelFromConfiguration(submissionId, testFormConfiguration, 'testScopeUUID');
|
||||
|
||||
expect(formModel[0] instanceof DynamicRowGroupModel).toBe(true);
|
||||
expect((formModel[0] as DynamicRowGroupModel).group.length).toBe(3);
|
||||
@@ -427,7 +430,7 @@ describe('FormBuilderService test suite', () => {
|
||||
});
|
||||
|
||||
it('should return form\'s fields value from form model', () => {
|
||||
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
|
||||
const formModel = service.modelFromConfiguration(submissionId, testFormConfiguration, 'testScopeUUID');
|
||||
let value = {} as any;
|
||||
|
||||
expect(service.getValueFromModel(formModel)).toEqual(value);
|
||||
@@ -448,7 +451,7 @@ describe('FormBuilderService test suite', () => {
|
||||
});
|
||||
|
||||
it('should clear all form\'s fields value', () => {
|
||||
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
|
||||
const formModel = service.modelFromConfiguration(submissionId, testFormConfiguration, 'testScopeUUID');
|
||||
const value = {} as any;
|
||||
|
||||
((formModel[0] as DynamicRowGroupModel).get(1) as DsDynamicInputModel).valueUpdates.next('test');
|
||||
@@ -460,7 +463,7 @@ describe('FormBuilderService test suite', () => {
|
||||
});
|
||||
|
||||
it('should return true when model has a custom group model as parent', () => {
|
||||
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
|
||||
const formModel = service.modelFromConfiguration(submissionId, testFormConfiguration, 'testScopeUUID');
|
||||
let model = service.findById('dc_identifier_QUALDROP_VALUE', formModel);
|
||||
let modelParent = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
|
||||
model.parent = modelParent;
|
||||
@@ -489,7 +492,7 @@ describe('FormBuilderService test suite', () => {
|
||||
});
|
||||
|
||||
it('should return true when model value is a map', () => {
|
||||
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
|
||||
const formModel = service.modelFromConfiguration(submissionId, testFormConfiguration, 'testScopeUUID');
|
||||
const model = service.findById('dc_identifier_QUALDROP_VALUE', formModel);
|
||||
const modelParent = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
|
||||
model.parent = modelParent;
|
||||
@@ -498,7 +501,7 @@ describe('FormBuilderService test suite', () => {
|
||||
});
|
||||
|
||||
it('should return true when model is a Qualdrop Group', () => {
|
||||
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
|
||||
const formModel = service.modelFromConfiguration(submissionId, testFormConfiguration, 'testScopeUUID');
|
||||
let model = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
|
||||
|
||||
expect(service.isQualdropGroup(model)).toBe(true);
|
||||
@@ -509,7 +512,7 @@ describe('FormBuilderService test suite', () => {
|
||||
});
|
||||
|
||||
it('should return true when model is a Custom or List Group', () => {
|
||||
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
|
||||
const formModel = service.modelFromConfiguration(submissionId, testFormConfiguration, 'testScopeUUID');
|
||||
let model = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
|
||||
|
||||
expect(service.isCustomOrListGroup(model)).toBe(true);
|
||||
@@ -528,7 +531,7 @@ describe('FormBuilderService test suite', () => {
|
||||
});
|
||||
|
||||
it('should return true when model is a Custom Group', () => {
|
||||
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
|
||||
const formModel = service.modelFromConfiguration(submissionId, testFormConfiguration, 'testScopeUUID');
|
||||
let model = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
|
||||
|
||||
expect(service.isCustomGroup(model)).toBe(true);
|
||||
|
@@ -10,7 +10,7 @@ import {
|
||||
DynamicFormArrayModel,
|
||||
DynamicFormControlModel,
|
||||
DynamicFormGroupModel,
|
||||
DynamicFormService,
|
||||
DynamicFormService, DynamicFormValidationService,
|
||||
DynamicPathable,
|
||||
JSONUtils,
|
||||
} from '@ng-dynamic-forms/core';
|
||||
@@ -33,6 +33,13 @@ import { isNgbDateStruct } from '../../date.util';
|
||||
@Injectable()
|
||||
export class FormBuilderService extends DynamicFormService {
|
||||
|
||||
constructor(
|
||||
validationService: DynamicFormValidationService,
|
||||
protected rowParser: RowParser
|
||||
) {
|
||||
super(validationService);
|
||||
}
|
||||
|
||||
findById(id: string, groupModel: DynamicFormControlModel[], arrayIndex = null): DynamicFormControlModel | null {
|
||||
|
||||
let result = null;
|
||||
@@ -198,13 +205,13 @@ export class FormBuilderService extends DynamicFormService {
|
||||
return result;
|
||||
}
|
||||
|
||||
modelFromConfiguration(json: string | SubmissionFormsModel, scopeUUID: string, initFormValues: any = {}, submissionScope?: string, readOnly = false): DynamicFormControlModel[] | never {
|
||||
modelFromConfiguration(submissionId: string, json: string | SubmissionFormsModel, scopeUUID: string, sectionData: any = {}, submissionScope?: string, readOnly = false): DynamicFormControlModel[] | never {
|
||||
let rows: DynamicFormControlModel[] = [];
|
||||
const rawData = typeof json === 'string' ? JSON.parse(json, JSONUtils.parseReviver) : json;
|
||||
|
||||
if (rawData.rows && !isEmpty(rawData.rows)) {
|
||||
rawData.rows.forEach((currentRow) => {
|
||||
const rowParsed = new RowParser(currentRow, scopeUUID, initFormValues, submissionScope, readOnly).parse();
|
||||
const rowParsed = this.rowParser.parse(submissionId, currentRow, scopeUUID, sectionData, submissionScope, readOnly);
|
||||
if (isNotNull(rowParsed)) {
|
||||
if (Array.isArray(rowParsed)) {
|
||||
rows = rows.concat(rowParsed);
|
||||
|
@@ -1,4 +1,11 @@
|
||||
import { FieldParser } from './field-parser';
|
||||
import { Inject } from '@angular/core';
|
||||
import {
|
||||
CONFIG_DATA,
|
||||
FieldParser,
|
||||
INIT_FORM_VALUES,
|
||||
PARSER_OPTIONS,
|
||||
SUBMISSION_ID
|
||||
} from './field-parser';
|
||||
import { FormFieldModel } from '../models/form-field.model';
|
||||
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||
import { DynamicFormControlLayout, DynamicInputModel, DynamicInputModelConfig } from '@ng-dynamic-forms/core';
|
||||
@@ -14,13 +21,15 @@ import { ParserOptions } from './parser-options';
|
||||
|
||||
export class ConcatFieldParser extends FieldParser {
|
||||
|
||||
constructor(protected configData: FormFieldModel,
|
||||
protected initFormValues,
|
||||
protected parserOptions: ParserOptions,
|
||||
constructor(
|
||||
@Inject(SUBMISSION_ID) submissionId: string,
|
||||
@Inject(CONFIG_DATA) configData: FormFieldModel,
|
||||
@Inject(INIT_FORM_VALUES) initFormValues,
|
||||
@Inject(PARSER_OPTIONS) parserOptions: ParserOptions,
|
||||
protected separator: string,
|
||||
protected firstPlaceholder: string = null,
|
||||
protected secondPlaceholder: string = null) {
|
||||
super(configData, initFormValues, parserOptions);
|
||||
super(submissionId, configData, initFormValues, parserOptions);
|
||||
|
||||
this.separator = separator;
|
||||
this.firstPlaceholder = firstPlaceholder;
|
||||
|
@@ -1,6 +1,4 @@
|
||||
import { FormFieldModel } from '../models/form-field.model';
|
||||
import { DynamicConcatModel } from '../ds-dynamic-form-ui/models/ds-dynamic-concat.model';
|
||||
import { SeriesFieldParser } from './series-field-parser';
|
||||
import { DateFieldParser } from './date-field-parser';
|
||||
import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/date-picker/date-picker.model';
|
||||
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||
@@ -10,6 +8,7 @@ describe('DateFieldParser test suite', () => {
|
||||
let field: FormFieldModel;
|
||||
let initFormValues: any = {};
|
||||
|
||||
const submissionId = '1234';
|
||||
const parserOptions: ParserOptions = {
|
||||
readOnly: false,
|
||||
submissionScope: null,
|
||||
@@ -37,13 +36,13 @@ describe('DateFieldParser test suite', () => {
|
||||
});
|
||||
|
||||
it('should init parser properly', () => {
|
||||
const parser = new DateFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
expect(parser instanceof DateFieldParser).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a DynamicDsDatePickerModel object when repeatable option is false', () => {
|
||||
const parser = new DateFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
@@ -56,7 +55,7 @@ describe('DateFieldParser test suite', () => {
|
||||
};
|
||||
const expectedValue = '1983-11-18';
|
||||
|
||||
const parser = new DateFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import { ParserOptions } from './parser-options';
|
||||
describe('DropdownFieldParser test suite', () => {
|
||||
let field: FormFieldModel;
|
||||
|
||||
const submissionId = '1234';
|
||||
const initFormValues = {};
|
||||
const parserOptions: ParserOptions = {
|
||||
readOnly: false,
|
||||
@@ -35,13 +36,13 @@ describe('DropdownFieldParser test suite', () => {
|
||||
});
|
||||
|
||||
it('should init parser properly', () => {
|
||||
const parser = new DropdownFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
expect(parser instanceof DropdownFieldParser).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a DynamicScrollableDropdownModel object when repeatable option is false', () => {
|
||||
const parser = new DropdownFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
@@ -50,7 +51,7 @@ describe('DropdownFieldParser test suite', () => {
|
||||
|
||||
it('should throw when authority is not passed', () => {
|
||||
field.selectableMetadata[0].authority = null;
|
||||
const parser = new DropdownFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
expect(() => parser.parse())
|
||||
.toThrow();
|
||||
|
@@ -1,4 +1,12 @@
|
||||
import { FieldParser } from './field-parser';
|
||||
import { Inject } from '@angular/core';
|
||||
import { FormFieldModel } from '../models/form-field.model';
|
||||
import {
|
||||
CONFIG_DATA,
|
||||
FieldParser,
|
||||
INIT_FORM_VALUES,
|
||||
PARSER_OPTIONS,
|
||||
SUBMISSION_ID
|
||||
} from './field-parser';
|
||||
import { DynamicFormControlLayout, } from '@ng-dynamic-forms/core';
|
||||
import {
|
||||
DynamicScrollableDropdownModel,
|
||||
@@ -6,9 +14,19 @@ import {
|
||||
} from '../ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
||||
import { isNotEmpty } from '../../../empty.util';
|
||||
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||
import { ParserOptions } from './parser-options';
|
||||
|
||||
export class DropdownFieldParser extends FieldParser {
|
||||
|
||||
constructor(
|
||||
@Inject(SUBMISSION_ID) submissionId: string,
|
||||
@Inject(CONFIG_DATA) configData: FormFieldModel,
|
||||
@Inject(INIT_FORM_VALUES) initFormValues,
|
||||
@Inject(PARSER_OPTIONS) parserOptions: ParserOptions
|
||||
) {
|
||||
super(submissionId, configData, initFormValues, parserOptions)
|
||||
}
|
||||
|
||||
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any {
|
||||
const dropdownModelConfig: DynamicScrollableDropdownModelConfig = this.initModel(null, label);
|
||||
let layout: DynamicFormControlLayout;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { Inject, InjectionToken } from '@angular/core';
|
||||
import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../empty.util';
|
||||
import { FormFieldModel } from '../models/form-field.model';
|
||||
|
||||
@@ -13,12 +14,21 @@ import { setLayout } from './parser.utils';
|
||||
import { AuthorityOptions } from '../../../../core/integration/models/authority-options.model';
|
||||
import { ParserOptions } from './parser-options';
|
||||
|
||||
export const SUBMISSION_ID: InjectionToken<string> = new InjectionToken<string>('submissionId');
|
||||
export const CONFIG_DATA: InjectionToken<FormFieldModel> = new InjectionToken<FormFieldModel>('configData');
|
||||
export const INIT_FORM_VALUES:InjectionToken<any> = new InjectionToken<any>('initFormValues');
|
||||
export const PARSER_OPTIONS: InjectionToken<ParserOptions> = new InjectionToken<ParserOptions>('parserOptions');
|
||||
|
||||
export abstract class FieldParser {
|
||||
|
||||
protected fieldId: string;
|
||||
|
||||
constructor(protected configData: FormFieldModel, protected initFormValues, protected parserOptions: ParserOptions) {
|
||||
}
|
||||
constructor(
|
||||
@Inject(SUBMISSION_ID) protected submissionId: string,
|
||||
@Inject(CONFIG_DATA) protected configData: FormFieldModel,
|
||||
@Inject(INIT_FORM_VALUES) protected initFormValues: any,
|
||||
@Inject(PARSER_OPTIONS) protected parserOptions: ParserOptions
|
||||
) {}
|
||||
|
||||
public abstract modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any;
|
||||
|
||||
|
@@ -9,6 +9,7 @@ describe('ListFieldParser test suite', () => {
|
||||
let field: FormFieldModel;
|
||||
let initFormValues = {};
|
||||
|
||||
const submissionId = '1234';
|
||||
const parserOptions: ParserOptions = {
|
||||
readOnly: false,
|
||||
submissionScope: 'testScopeUUID',
|
||||
@@ -37,13 +38,13 @@ describe('ListFieldParser test suite', () => {
|
||||
});
|
||||
|
||||
it('should init parser properly', () => {
|
||||
const parser = new ListFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
expect(parser instanceof ListFieldParser).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a DynamicListCheckboxGroupModel object when repeatable option is true', () => {
|
||||
const parser = new ListFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
@@ -52,7 +53,7 @@ describe('ListFieldParser test suite', () => {
|
||||
|
||||
it('should return a DynamicListRadioGroupModel object when repeatable option is false', () => {
|
||||
field.repeatable = false;
|
||||
const parser = new ListFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
@@ -65,7 +66,7 @@ describe('ListFieldParser test suite', () => {
|
||||
};
|
||||
const expectedValue = [new FormFieldMetadataValueObject('test type')];
|
||||
|
||||
const parser = new ListFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
|
@@ -8,6 +8,7 @@ describe('LookupFieldParser test suite', () => {
|
||||
let field: FormFieldModel;
|
||||
let initFormValues = {};
|
||||
|
||||
const submissionId = '1234';
|
||||
const parserOptions: ParserOptions = {
|
||||
readOnly: false,
|
||||
submissionScope: 'testScopeUUID',
|
||||
@@ -36,13 +37,13 @@ describe('LookupFieldParser test suite', () => {
|
||||
});
|
||||
|
||||
it('should init parser properly', () => {
|
||||
const parser = new LookupFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
expect(parser instanceof LookupFieldParser).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a DynamicLookupModel object when repeatable option is false', () => {
|
||||
const parser = new LookupFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
@@ -55,7 +56,7 @@ describe('LookupFieldParser test suite', () => {
|
||||
};
|
||||
const expectedValue = new FormFieldMetadataValueObject('test journal');
|
||||
|
||||
const parser = new LookupFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
|
@@ -1,7 +1,5 @@
|
||||
import { FormFieldModel } from '../models/form-field.model';
|
||||
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||
import { LookupFieldParser } from './lookup-field-parser';
|
||||
import { DynamicLookupModel } from '../ds-dynamic-form-ui/models/lookup/dynamic-lookup.model';
|
||||
import { LookupNameFieldParser } from './lookup-name-field-parser';
|
||||
import { DynamicLookupNameModel } from '../ds-dynamic-form-ui/models/lookup/dynamic-lookup-name.model';
|
||||
import { ParserOptions } from './parser-options';
|
||||
@@ -10,6 +8,7 @@ describe('LookupNameFieldParser test suite', () => {
|
||||
let field: FormFieldModel;
|
||||
let initFormValues = {};
|
||||
|
||||
const submissionId = '1234';
|
||||
const parserOptions: ParserOptions = {
|
||||
readOnly: false,
|
||||
submissionScope: 'testScopeUUID',
|
||||
@@ -38,13 +37,13 @@ describe('LookupNameFieldParser test suite', () => {
|
||||
});
|
||||
|
||||
it('should init parser properly', () => {
|
||||
const parser = new LookupNameFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
expect(parser instanceof LookupNameFieldParser).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a DynamicLookupNameModel object when repeatable option is false', () => {
|
||||
const parser = new LookupNameFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
@@ -57,7 +56,7 @@ describe('LookupNameFieldParser test suite', () => {
|
||||
};
|
||||
const expectedValue = new FormFieldMetadataValueObject('test author');
|
||||
|
||||
const parser = new LookupNameFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
|
@@ -10,6 +10,7 @@ describe('NameFieldParser test suite', () => {
|
||||
let field3: FormFieldModel;
|
||||
let initFormValues: any = {};
|
||||
|
||||
const submissionId = '1234';
|
||||
const parserOptions: ParserOptions = {
|
||||
readOnly: false,
|
||||
submissionScope: 'testScopeUUID',
|
||||
@@ -69,13 +70,13 @@ describe('NameFieldParser test suite', () => {
|
||||
});
|
||||
|
||||
it('should init parser properly', () => {
|
||||
const parser = new NameFieldParser(field1, initFormValues, parserOptions);
|
||||
const parser = new NameFieldParser(submissionId, field1, initFormValues, parserOptions);
|
||||
|
||||
expect(parser instanceof NameFieldParser).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a DynamicConcatModel object when repeatable option is false', () => {
|
||||
const parser = new NameFieldParser(field2, initFormValues, parserOptions);
|
||||
const parser = new NameFieldParser(submissionId, field2, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
@@ -83,7 +84,7 @@ describe('NameFieldParser test suite', () => {
|
||||
});
|
||||
|
||||
it('should return a DynamicConcatModel object with the correct separator', () => {
|
||||
const parser = new NameFieldParser(field2, initFormValues, parserOptions);
|
||||
const parser = new NameFieldParser(submissionId, field2, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
@@ -96,7 +97,7 @@ describe('NameFieldParser test suite', () => {
|
||||
};
|
||||
const expectedValue = new FormFieldMetadataValueObject('test, name');
|
||||
|
||||
const parser = new NameFieldParser(field1, initFormValues, parserOptions);
|
||||
const parser = new NameFieldParser(submissionId, field1, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
|
@@ -1,10 +1,17 @@
|
||||
import { Inject } from '@angular/core';
|
||||
import { FormFieldModel } from '../models/form-field.model';
|
||||
import { ConcatFieldParser } from './concat-field-parser';
|
||||
import { CONFIG_DATA, INIT_FORM_VALUES, PARSER_OPTIONS, SUBMISSION_ID } from './field-parser';
|
||||
import { ParserOptions } from './parser-options';
|
||||
|
||||
export class NameFieldParser extends ConcatFieldParser {
|
||||
|
||||
constructor(protected configData: FormFieldModel, protected initFormValues, protected parserOptions: ParserOptions) {
|
||||
super(configData, initFormValues, parserOptions, ',', 'form.last-name', 'form.first-name');
|
||||
constructor(
|
||||
@Inject(SUBMISSION_ID) submissionId: string,
|
||||
@Inject(CONFIG_DATA) configData: FormFieldModel,
|
||||
@Inject(INIT_FORM_VALUES) initFormValues,
|
||||
@Inject(PARSER_OPTIONS) parserOptions: ParserOptions
|
||||
) {
|
||||
super(submissionId, configData, initFormValues, parserOptions, ',', 'form.last-name', 'form.first-name');
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ describe('OneboxFieldParser test suite', () => {
|
||||
let field2: FormFieldModel;
|
||||
let field3: FormFieldModel;
|
||||
|
||||
const submissionId = '1234';
|
||||
const initFormValues = {};
|
||||
const parserOptions: ParserOptions = {
|
||||
readOnly: false,
|
||||
@@ -70,13 +71,13 @@ describe('OneboxFieldParser test suite', () => {
|
||||
});
|
||||
|
||||
it('should init parser properly', () => {
|
||||
const parser = new OneboxFieldParser(field1, initFormValues, parserOptions);
|
||||
const parser = new OneboxFieldParser(submissionId, field1, initFormValues, parserOptions);
|
||||
|
||||
expect(parser instanceof OneboxFieldParser).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a DynamicQualdropModel object when selectableMetadata is multiple', () => {
|
||||
const parser = new OneboxFieldParser(field2, initFormValues, parserOptions);
|
||||
const parser = new OneboxFieldParser(submissionId, field2, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
@@ -84,7 +85,7 @@ describe('OneboxFieldParser test suite', () => {
|
||||
});
|
||||
|
||||
it('should return a DsDynamicInputModel object when selectableMetadata is not multiple', () => {
|
||||
const parser = new OneboxFieldParser(field3, initFormValues, parserOptions);
|
||||
const parser = new OneboxFieldParser(submissionId, field3, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
@@ -92,7 +93,7 @@ describe('OneboxFieldParser test suite', () => {
|
||||
});
|
||||
|
||||
it('should return a DynamicTypeaheadModel object when selectableMetadata has authority', () => {
|
||||
const parser = new OneboxFieldParser(field1, initFormValues, parserOptions);
|
||||
const parser = new OneboxFieldParser(submissionId, field1, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
|
@@ -1,6 +1,12 @@
|
||||
import { StaticProvider } from '@angular/core';
|
||||
import { ParserType } from './parser-type';
|
||||
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||
import { FieldParser } from './field-parser';
|
||||
import {
|
||||
CONFIG_DATA,
|
||||
FieldParser,
|
||||
INIT_FORM_VALUES,
|
||||
PARSER_OPTIONS,
|
||||
SUBMISSION_ID
|
||||
} from './field-parser';
|
||||
import { DateFieldParser } from './date-field-parser';
|
||||
import { DropdownFieldParser } from './dropdown-field-parser';
|
||||
import { RelationGroupFieldParser } from './relation-group-field-parser';
|
||||
@@ -13,41 +19,92 @@ import { SeriesFieldParser } from './series-field-parser';
|
||||
import { TagFieldParser } from './tag-field-parser';
|
||||
import { TextareaFieldParser } from './textarea-field-parser';
|
||||
|
||||
const fieldParserDeps = [
|
||||
SUBMISSION_ID,
|
||||
CONFIG_DATA,
|
||||
INIT_FORM_VALUES,
|
||||
PARSER_OPTIONS,
|
||||
];
|
||||
|
||||
export class ParserFactory {
|
||||
public static getConstructor(type: ParserType): GenericConstructor<FieldParser> {
|
||||
public static getProvider(type: ParserType): StaticProvider {
|
||||
switch (type) {
|
||||
case ParserType.Date: {
|
||||
return DateFieldParser
|
||||
return {
|
||||
provide: FieldParser,
|
||||
useClass: DateFieldParser,
|
||||
deps: [...fieldParserDeps]
|
||||
}
|
||||
}
|
||||
case ParserType.Dropdown: {
|
||||
return DropdownFieldParser
|
||||
return {
|
||||
provide: FieldParser,
|
||||
useClass: DropdownFieldParser,
|
||||
deps: [...fieldParserDeps]
|
||||
}
|
||||
}
|
||||
case ParserType.RelationGroup: {
|
||||
return RelationGroupFieldParser
|
||||
return {
|
||||
provide: FieldParser,
|
||||
useClass: RelationGroupFieldParser,
|
||||
deps: [...fieldParserDeps]
|
||||
}
|
||||
}
|
||||
case ParserType.List: {
|
||||
return ListFieldParser
|
||||
return {
|
||||
provide: FieldParser,
|
||||
useClass: ListFieldParser,
|
||||
deps: [...fieldParserDeps]
|
||||
}
|
||||
}
|
||||
case ParserType.Lookup: {
|
||||
return LookupFieldParser
|
||||
return {
|
||||
provide: FieldParser,
|
||||
useClass: LookupFieldParser,
|
||||
deps: [...fieldParserDeps]
|
||||
}
|
||||
}
|
||||
case ParserType.LookupName: {
|
||||
return LookupNameFieldParser
|
||||
return {
|
||||
provide: FieldParser,
|
||||
useClass: LookupNameFieldParser,
|
||||
deps: [...fieldParserDeps]
|
||||
}
|
||||
}
|
||||
case ParserType.Onebox: {
|
||||
return OneboxFieldParser
|
||||
return {
|
||||
provide: FieldParser,
|
||||
useClass: OneboxFieldParser,
|
||||
deps: [...fieldParserDeps]
|
||||
}
|
||||
}
|
||||
case ParserType.Name: {
|
||||
return NameFieldParser
|
||||
return {
|
||||
provide: FieldParser,
|
||||
useClass: NameFieldParser,
|
||||
deps: [...fieldParserDeps]
|
||||
}
|
||||
}
|
||||
case ParserType.Series: {
|
||||
return SeriesFieldParser
|
||||
return {
|
||||
provide: FieldParser,
|
||||
useClass: SeriesFieldParser,
|
||||
deps: [...fieldParserDeps]
|
||||
}
|
||||
}
|
||||
case ParserType.Tag: {
|
||||
return TagFieldParser
|
||||
return {
|
||||
provide: FieldParser,
|
||||
useClass: TagFieldParser,
|
||||
deps: [...fieldParserDeps]
|
||||
}
|
||||
}
|
||||
case ParserType.Textarea: {
|
||||
return TextareaFieldParser
|
||||
return {
|
||||
provide: FieldParser,
|
||||
useClass: TextareaFieldParser,
|
||||
deps: [...fieldParserDeps]
|
||||
}
|
||||
}
|
||||
|
||||
default: {
|
||||
|
@@ -8,6 +8,7 @@ describe('RelationGroupFieldParser test suite', () => {
|
||||
let field: FormFieldModel;
|
||||
let initFormValues = {};
|
||||
|
||||
const submissionId = '1234';
|
||||
const parserOptions: ParserOptions = {
|
||||
readOnly: false,
|
||||
submissionScope: 'testScopeUUID',
|
||||
@@ -71,13 +72,13 @@ describe('RelationGroupFieldParser test suite', () => {
|
||||
});
|
||||
|
||||
it('should init parser properly', () => {
|
||||
const parser = new RelationGroupFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
expect(parser instanceof RelationGroupFieldParser).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a DynamicRelationGroupModel object', () => {
|
||||
const parser = new RelationGroupFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
@@ -86,7 +87,7 @@ describe('RelationGroupFieldParser test suite', () => {
|
||||
|
||||
it('should throw when rows configuration is empty', () => {
|
||||
field.rows = null;
|
||||
const parser = new RelationGroupFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
expect(() => parser.parse())
|
||||
.toThrow();
|
||||
@@ -97,7 +98,7 @@ describe('RelationGroupFieldParser test suite', () => {
|
||||
author: [new FormFieldMetadataValueObject('test author')],
|
||||
affiliation: [new FormFieldMetadataValueObject('test affiliation')]
|
||||
};
|
||||
const parser = new RelationGroupFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
const expectedValue = [{
|
||||
|
@@ -15,6 +15,7 @@ export class RelationGroupFieldParser extends FieldParser {
|
||||
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean) {
|
||||
const modelConfiguration: DynamicRelationGroupModelConfig = this.initModel(null, label);
|
||||
|
||||
modelConfiguration.submissionId = this.submissionId;
|
||||
modelConfiguration.scopeUUID = this.parserOptions.authorityUuid;
|
||||
modelConfiguration.submissionScope = this.parserOptions.submissionScope;
|
||||
if (this.configData && this.configData.rows && this.configData.rows.length > 0) {
|
||||
|
@@ -17,6 +17,7 @@ describe('RowParser test suite', () => {
|
||||
let row9: FormRowModel;
|
||||
let row10: FormRowModel;
|
||||
|
||||
const submissionId = '1234';
|
||||
const scopeUUID = 'testScopeUUID';
|
||||
const initFormValues = {};
|
||||
const submissionScope = 'WORKSPACE';
|
||||
@@ -328,76 +329,98 @@ describe('RowParser test suite', () => {
|
||||
});
|
||||
|
||||
it('should init parser properly', () => {
|
||||
let parser = new RowParser(row1, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(parser instanceof RowParser).toBe(true);
|
||||
|
||||
parser = new RowParser(row2, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(parser instanceof RowParser).toBe(true);
|
||||
|
||||
parser = new RowParser(row3, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(parser instanceof RowParser).toBe(true);
|
||||
|
||||
parser = new RowParser(row4, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(parser instanceof RowParser).toBe(true);
|
||||
|
||||
parser = new RowParser(row5, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(parser instanceof RowParser).toBe(true);
|
||||
|
||||
parser = new RowParser(row6, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(parser instanceof RowParser).toBe(true);
|
||||
|
||||
parser = new RowParser(row7, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(parser instanceof RowParser).toBe(true);
|
||||
|
||||
parser = new RowParser(row8, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(parser instanceof RowParser).toBe(true);
|
||||
|
||||
parser = new RowParser(row9, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(parser instanceof RowParser).toBe(true);
|
||||
|
||||
parser = new RowParser(row10, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
const parser = new RowParser(undefined);
|
||||
|
||||
expect(parser instanceof RowParser).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a DynamicRowGroupModel object', () => {
|
||||
const parser = new RowParser(row1, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
describe('parse', () => {
|
||||
it('should return a DynamicRowGroupModel object', () => {
|
||||
const parser = new RowParser(undefined);
|
||||
|
||||
const rowModel = parser.parse();
|
||||
const rowModel = parser.parse(submissionId, row1, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(rowModel instanceof DynamicRowGroupModel).toBe(true);
|
||||
});
|
||||
expect(rowModel instanceof DynamicRowGroupModel).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a row with three fields', () => {
|
||||
const parser = new RowParser(row1, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
it('should return a row with three fields', () => {
|
||||
const parser = new RowParser(undefined);
|
||||
|
||||
const rowModel = parser.parse();
|
||||
const rowModel = parser.parse(submissionId, row1, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect((rowModel as DynamicRowGroupModel).group.length).toBe(3);
|
||||
});
|
||||
expect((rowModel as DynamicRowGroupModel).group.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should return a DynamicRowArrayModel object', () => {
|
||||
const parser = new RowParser(row2, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
it('should return a DynamicRowArrayModel object', () => {
|
||||
const parser = new RowParser(undefined);
|
||||
|
||||
const rowModel = parser.parse();
|
||||
const rowModel = parser.parse(submissionId, row2, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(rowModel instanceof DynamicRowArrayModel).toBe(true);
|
||||
});
|
||||
expect(rowModel instanceof DynamicRowArrayModel).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a row that contains only scoped fields', () => {
|
||||
const parser = new RowParser(row3, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
it('should return a row that contains only scoped fields', () => {
|
||||
const parser = new RowParser(undefined);
|
||||
|
||||
const rowModel = parser.parse();
|
||||
const rowModel = parser.parse(submissionId, row3, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect((rowModel as DynamicRowGroupModel).group.length).toBe(1);
|
||||
expect((rowModel as DynamicRowGroupModel).group.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should be able to parse a dropdown combo field', () => {
|
||||
const parser = new RowParser(undefined);
|
||||
|
||||
const rowModel = parser.parse(submissionId, row4, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(rowModel).toBeDefined();
|
||||
});
|
||||
|
||||
it('should be able to parse a lookup-name field', () => {
|
||||
const parser = new RowParser(undefined);
|
||||
|
||||
const rowModel = parser.parse(submissionId, row5, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(rowModel).toBeDefined();
|
||||
});
|
||||
|
||||
it('should be able to parse a list field', () => {
|
||||
const parser = new RowParser(undefined);
|
||||
|
||||
const rowModel = parser.parse(submissionId, row6, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(rowModel).toBeDefined();
|
||||
});
|
||||
|
||||
it('should be able to parse a date field', () => {
|
||||
const parser = new RowParser(undefined);
|
||||
|
||||
const rowModel = parser.parse(submissionId, row7, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(rowModel).toBeDefined();
|
||||
});
|
||||
|
||||
it('should be able to parse a tag field', () => {
|
||||
const parser = new RowParser(undefined);
|
||||
|
||||
const rowModel = parser.parse(submissionId, row8, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(rowModel).toBeDefined();
|
||||
});
|
||||
|
||||
it('should be able to parse a textarea field', () => {
|
||||
const parser = new RowParser(undefined);
|
||||
|
||||
const rowModel = parser.parse(submissionId, row9, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(rowModel).toBeDefined();
|
||||
});
|
||||
|
||||
it('should be able to parse a group field', () => {
|
||||
const parser = new RowParser(undefined);
|
||||
|
||||
const rowModel = parser.parse(submissionId, row10, scopeUUID, initFormValues, submissionScope, readOnly);
|
||||
|
||||
expect(rowModel).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,30 +1,42 @@
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core';
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import {
|
||||
DYNAMIC_FORM_CONTROL_TYPE_ARRAY,
|
||||
DynamicFormGroupModelConfig
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { uniqueId } from 'lodash';
|
||||
|
||||
import { IntegrationSearchOptions } from '../../../../core/integration/models/integration-options.model';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from '../ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model';
|
||||
import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model';
|
||||
import { isEmpty } from '../../../empty.util';
|
||||
import { setLayout } from './parser.utils';
|
||||
import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from '../ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model';
|
||||
import { FormFieldModel } from '../models/form-field.model';
|
||||
import { ParserType } from './parser-type';
|
||||
import { ParserOptions } from './parser-options';
|
||||
import {
|
||||
CONFIG_DATA,
|
||||
FieldParser,
|
||||
INIT_FORM_VALUES,
|
||||
PARSER_OPTIONS,
|
||||
SUBMISSION_ID
|
||||
} from './field-parser';
|
||||
import { ParserFactory } from './parser-factory';
|
||||
import { ParserOptions } from './parser-options';
|
||||
import { ParserType } from './parser-type';
|
||||
import { setLayout } from './parser.utils';
|
||||
|
||||
export const ROW_ID_PREFIX = 'df-row-group-config-';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class RowParser {
|
||||
protected authorityOptions: IntegrationSearchOptions;
|
||||
|
||||
constructor(protected rowData,
|
||||
protected scopeUUID,
|
||||
protected initFormValues: any,
|
||||
protected submissionScope,
|
||||
protected readOnly: boolean) {
|
||||
this.authorityOptions = new IntegrationSearchOptions(scopeUUID);
|
||||
constructor(private parentInjector: Injector) {
|
||||
}
|
||||
|
||||
public parse(): DynamicRowGroupModel {
|
||||
public parse(submissionId: string,
|
||||
rowData,
|
||||
scopeUUID,
|
||||
initFormValues: any,
|
||||
submissionScope,
|
||||
readOnly: boolean): DynamicRowGroupModel {
|
||||
let fieldModel: any = null;
|
||||
let parsedResult = null;
|
||||
const config: DynamicFormGroupModelConfig = {
|
||||
@@ -32,31 +44,44 @@ export class RowParser {
|
||||
group: [],
|
||||
};
|
||||
|
||||
const scopedFields: FormFieldModel[] = this.filterScopedFields(this.rowData.fields);
|
||||
const authorityOptions = new IntegrationSearchOptions(scopeUUID);
|
||||
|
||||
const scopedFields: FormFieldModel[] = this.filterScopedFields(rowData.fields, submissionScope);
|
||||
|
||||
const layoutDefaultGridClass = ' col-sm-' + Math.trunc(12 / scopedFields.length);
|
||||
const layoutClass = ' d-flex flex-column justify-content-start';
|
||||
|
||||
const parserOptions: ParserOptions = {
|
||||
readOnly: this.readOnly,
|
||||
submissionScope: this.submissionScope,
|
||||
authorityUuid: this.authorityOptions.uuid
|
||||
readOnly: readOnly,
|
||||
submissionScope: submissionScope,
|
||||
authorityUuid: authorityOptions.uuid
|
||||
};
|
||||
|
||||
// Iterate over row's fields
|
||||
scopedFields.forEach((fieldData: FormFieldModel) => {
|
||||
|
||||
const layoutFieldClass = (fieldData.style || layoutDefaultGridClass) + layoutClass;
|
||||
const parserCo = ParserFactory.getConstructor(fieldData.input.type as ParserType);
|
||||
if (parserCo) {
|
||||
fieldModel = new parserCo(fieldData, this.initFormValues, parserOptions).parse();
|
||||
const parserProvider = ParserFactory.getProvider(fieldData.input.type as ParserType);
|
||||
if (parserProvider) {
|
||||
const fieldInjector = Injector.create({
|
||||
providers: [
|
||||
parserProvider,
|
||||
{ provide: SUBMISSION_ID, useValue: submissionId },
|
||||
{ provide: CONFIG_DATA, useValue: fieldData },
|
||||
{ provide: INIT_FORM_VALUES, useValue: initFormValues },
|
||||
{ provide: PARSER_OPTIONS, useValue: parserOptions }
|
||||
],
|
||||
parent: this.parentInjector
|
||||
});
|
||||
|
||||
fieldModel = fieldInjector.get(FieldParser).parse();
|
||||
} else {
|
||||
throw new Error(`unknown form control model type "${fieldData.input.type}" defined for Input field with label "${fieldData.label}".`, );
|
||||
throw new Error(`unknown form control model type "${fieldData.input.type}" defined for Input field with label "${fieldData.label}".`,);
|
||||
}
|
||||
|
||||
if (fieldModel) {
|
||||
if (fieldModel.type === DYNAMIC_FORM_CONTROL_TYPE_ARRAY || fieldModel.type === DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP) {
|
||||
if (this.rowData.fields.length > 1) {
|
||||
if (rowData.fields.length > 1) {
|
||||
setLayout(fieldModel, 'grid', 'host', layoutFieldClass);
|
||||
config.group.push(fieldModel);
|
||||
// if (isEmpty(parsedResult)) {
|
||||
@@ -98,15 +123,15 @@ export class RowParser {
|
||||
return parsedResult;
|
||||
}
|
||||
|
||||
checksFieldScope(fieldScope) {
|
||||
return (isEmpty(fieldScope) || isEmpty(this.submissionScope) || fieldScope === this.submissionScope);
|
||||
checksFieldScope(fieldScope, submissionScope) {
|
||||
return (isEmpty(fieldScope) || isEmpty(submissionScope) || fieldScope === submissionScope);
|
||||
}
|
||||
|
||||
filterScopedFields(fields: FormFieldModel[]): FormFieldModel[] {
|
||||
filterScopedFields(fields: FormFieldModel[], submissionScope): FormFieldModel[] {
|
||||
const filteredFields: FormFieldModel[] = [];
|
||||
fields.forEach((field: FormFieldModel) => {
|
||||
// Whether field scope doesn't match the submission scope, skip it
|
||||
if (this.checksFieldScope(field.scope)) {
|
||||
if (this.checksFieldScope(field.scope, submissionScope)) {
|
||||
filteredFields.push(field);
|
||||
}
|
||||
});
|
||||
|
@@ -8,6 +8,7 @@ describe('SeriesFieldParser test suite', () => {
|
||||
let field: FormFieldModel;
|
||||
let initFormValues: any = {};
|
||||
|
||||
const submissionId = '1234';
|
||||
const parserOptions: ParserOptions = {
|
||||
readOnly: false,
|
||||
submissionScope: 'testScopeUUID',
|
||||
@@ -32,13 +33,13 @@ describe('SeriesFieldParser test suite', () => {
|
||||
});
|
||||
|
||||
it('should init parser properly', () => {
|
||||
const parser = new SeriesFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
expect(parser instanceof SeriesFieldParser).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a DynamicConcatModel object when repeatable option is false', () => {
|
||||
const parser = new SeriesFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
@@ -46,7 +47,7 @@ describe('SeriesFieldParser test suite', () => {
|
||||
});
|
||||
|
||||
it('should return a DynamicConcatModel object with the correct separator', () => {
|
||||
const parser = new SeriesFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
@@ -59,7 +60,7 @@ describe('SeriesFieldParser test suite', () => {
|
||||
};
|
||||
const expectedValue = new FormFieldMetadataValueObject('test; series');
|
||||
|
||||
const parser = new SeriesFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
|
@@ -1,10 +1,17 @@
|
||||
import { Inject } from '@angular/core';
|
||||
import { FormFieldModel } from '../models/form-field.model';
|
||||
import { ConcatFieldParser } from './concat-field-parser';
|
||||
import { CONFIG_DATA, INIT_FORM_VALUES, PARSER_OPTIONS, SUBMISSION_ID } from './field-parser';
|
||||
import { ParserOptions } from './parser-options';
|
||||
|
||||
export class SeriesFieldParser extends ConcatFieldParser {
|
||||
|
||||
constructor(protected configData: FormFieldModel, protected initFormValues, protected parserOptions: ParserOptions) {
|
||||
super(configData, initFormValues, parserOptions, ';');
|
||||
constructor(
|
||||
@Inject(SUBMISSION_ID) submissionId: string,
|
||||
@Inject(CONFIG_DATA) configData: FormFieldModel,
|
||||
@Inject(INIT_FORM_VALUES) initFormValues,
|
||||
@Inject(PARSER_OPTIONS) parserOptions: ParserOptions
|
||||
) {
|
||||
super(submissionId, configData, initFormValues, parserOptions, ';');
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ describe('TagFieldParser test suite', () => {
|
||||
let field: FormFieldModel;
|
||||
let initFormValues: any = {};
|
||||
|
||||
const submissionId = '1234';
|
||||
const parserOptions: ParserOptions = {
|
||||
readOnly: false,
|
||||
submissionScope: 'testScopeUUID',
|
||||
@@ -36,13 +37,13 @@ describe('TagFieldParser test suite', () => {
|
||||
});
|
||||
|
||||
it('should init parser properly', () => {
|
||||
const parser = new TagFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
expect(parser instanceof TagFieldParser).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a DynamicTagModel object when repeatable option is false', () => {
|
||||
const parser = new TagFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
@@ -57,7 +58,7 @@ describe('TagFieldParser test suite', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const parser = new TagFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
|
@@ -8,6 +8,7 @@ describe('TextareaFieldParser test suite', () => {
|
||||
let field: FormFieldModel;
|
||||
let initFormValues: any = {};
|
||||
|
||||
const submissionId = '1234';
|
||||
const parserOptions: ParserOptions = {
|
||||
readOnly: false,
|
||||
submissionScope: null,
|
||||
@@ -34,13 +35,13 @@ describe('TextareaFieldParser test suite', () => {
|
||||
});
|
||||
|
||||
it('should init parser properly', () => {
|
||||
const parser = new TextareaFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
expect(parser instanceof TextareaFieldParser).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a DsDynamicTextAreaModel object when repeatable option is false', () => {
|
||||
const parser = new TextareaFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
@@ -55,7 +56,7 @@ describe('TextareaFieldParser test suite', () => {
|
||||
};
|
||||
const expectedValue ='test description';
|
||||
|
||||
const parser = new TextareaFieldParser(field, initFormValues, parserOptions);
|
||||
const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
/* tslint:disable:no-empty */
|
||||
export class AngularticsMock {
|
||||
public eventTrack(action, properties) { }
|
||||
public startTracking():void {}
|
||||
}
|
||||
|
@@ -115,6 +115,7 @@ const mockFormRowModel = {
|
||||
} as FormRowModel;
|
||||
|
||||
const relationGroupConfig = {
|
||||
submissionId: '1234',
|
||||
id: 'relationGroup',
|
||||
formConfiguration: [mockFormRowModel],
|
||||
mandatoryField: 'false',
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import {of as observableOf, Observable } from 'rxjs';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
import { RequestEntry } from '../../core/data/request.reducer';
|
||||
import SpyObj = jasmine.SpyObj;
|
||||
|
||||
export function getMockRequestService(requestEntry$: Observable<RequestEntry> = observableOf(new RequestEntry())): RequestService {
|
||||
export function getMockRequestService(requestEntry$: Observable<RequestEntry> = observableOf(new RequestEntry())): SpyObj<RequestService> {
|
||||
return jasmine.createSpyObj('requestService', {
|
||||
configure: false,
|
||||
generateRequestId: 'clients/b186e8ce-e99c-4183-bc9a-42b4821bdb78',
|
||||
|
@@ -145,6 +145,10 @@ import { ListableObjectDirective } from './object-collection/shared/listable-obj
|
||||
import { CommunitySearchResultGridElementComponent } from './object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component';
|
||||
import { CollectionSearchResultGridElementComponent } from './object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component';
|
||||
import { ItemMetadataRepresentationListElementComponent } from './object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component';
|
||||
import { PageWithSidebarComponent } from './sidebar/page-with-sidebar.component';
|
||||
import { SidebarDropdownComponent } from './sidebar/sidebar-dropdown.component';
|
||||
import { SidebarFilterComponent } from './sidebar/filter/sidebar-filter.component';
|
||||
import { SidebarFilterSelectedOptionComponent } from './sidebar/filter/sidebar-filter-selected-option.component';
|
||||
|
||||
const MODULES = [
|
||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||
@@ -228,6 +232,10 @@ const COMPONENTS = [
|
||||
ObjectCollectionComponent,
|
||||
PaginationComponent,
|
||||
SearchFormComponent,
|
||||
PageWithSidebarComponent,
|
||||
SidebarDropdownComponent,
|
||||
SidebarFilterComponent,
|
||||
SidebarFilterSelectedOptionComponent,
|
||||
ThumbnailComponent,
|
||||
GridThumbnailComponent,
|
||||
UploaderComponent,
|
||||
|
@@ -0,0 +1,6 @@
|
||||
<a class="d-flex flex-row" (click)="click.emit($event)">
|
||||
<label>
|
||||
<input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/>
|
||||
<span class="filter-value pl-1 text-capitalize">{{label}}</span>
|
||||
</label>
|
||||
</a>
|
@@ -0,0 +1,11 @@
|
||||
a {
|
||||
color: $body-color;
|
||||
|
||||
&:hover, &focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
span.badge {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-sidebar-filter-selected-option',
|
||||
styleUrls: ['./sidebar-filter-selected-option.component.scss'],
|
||||
templateUrl: './sidebar-filter-selected-option.component.html',
|
||||
})
|
||||
|
||||
/**
|
||||
* Represents a single selected option in a sidebar filter
|
||||
*/
|
||||
export class SidebarFilterSelectedOptionComponent {
|
||||
@Input() label:string;
|
||||
@Output() click:EventEmitter<any> = new EventEmitter<any>();
|
||||
}
|
74
src/app/shared/sidebar/filter/sidebar-filter.actions.ts
Normal file
74
src/app/shared/sidebar/filter/sidebar-filter.actions.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
|
||||
import { type } from '../../ngrx/type';
|
||||
|
||||
/**
|
||||
* For each action type in an action group, make a simple
|
||||
* enum object for all of this group's action types.
|
||||
*
|
||||
* The 'type' utility function coerces strings into string
|
||||
* literal types and runs a simple check to guarantee all
|
||||
* action types in the application are unique.
|
||||
*/
|
||||
export const SidebarFilterActionTypes = {
|
||||
INITIALIZE: type('dspace/sidebar-filter/INITIALIZE'),
|
||||
COLLAPSE: type('dspace/sidebar-filter/COLLAPSE'),
|
||||
EXPAND: type('dspace/sidebar-filter/EXPAND'),
|
||||
TOGGLE: type('dspace/sidebar-filter/TOGGLE'),
|
||||
};
|
||||
|
||||
export class SidebarFilterAction implements Action {
|
||||
/**
|
||||
* Name of the filter the action is performed on, used to identify the filter
|
||||
*/
|
||||
filterName: string;
|
||||
|
||||
/**
|
||||
* Type of action that will be performed
|
||||
*/
|
||||
type;
|
||||
|
||||
/**
|
||||
* Initialize with the filter's name
|
||||
* @param {string} name of the filter
|
||||
*/
|
||||
constructor(name: string) {
|
||||
this.filterName = name;
|
||||
}
|
||||
}
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
* Used to initialize a filter
|
||||
*/
|
||||
export class FilterInitializeAction extends SidebarFilterAction {
|
||||
type = SidebarFilterActionTypes.INITIALIZE;
|
||||
initiallyExpanded;
|
||||
|
||||
constructor(name:string, initiallyExpanded:boolean) {
|
||||
super(name);
|
||||
this.initiallyExpanded = initiallyExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to collapse a filter
|
||||
*/
|
||||
export class FilterCollapseAction extends SidebarFilterAction {
|
||||
type = SidebarFilterActionTypes.COLLAPSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to expand a filter
|
||||
*/
|
||||
export class FilterExpandAction extends SidebarFilterAction {
|
||||
type = SidebarFilterActionTypes.EXPAND;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to collapse a filter when it's expanded and expand it when it's collapsed
|
||||
*/
|
||||
export class FilterToggleAction extends SidebarFilterAction {
|
||||
type = SidebarFilterActionTypes.TOGGLE;
|
||||
}
|
||||
/* tslint:enable:max-classes-per-file */
|
26
src/app/shared/sidebar/filter/sidebar-filter.component.html
Normal file
26
src/app/shared/sidebar/filter/sidebar-filter.component.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<div class="facet-filter d-block mb-3 p-3">
|
||||
<div (click)="toggle()" class="filter-name">
|
||||
<h5 class="d-inline-block mb-0">
|
||||
{{ label | translate }}
|
||||
</h5>
|
||||
<span class="filter-toggle fas float-right"
|
||||
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'">
|
||||
</span>
|
||||
</div>
|
||||
<div [@slide]="(collapsed$ | async) ? 'collapsed' : 'expanded'"
|
||||
(@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)"
|
||||
class="sidebar-filter-wrapper" [ngClass]="{'closed' : closed}">
|
||||
<div>
|
||||
<div class="filters py-2">
|
||||
<ng-template *ngIf="!singleValue">
|
||||
<ds-sidebar-filter-selected-option
|
||||
*ngFor="let value of (selectedValues | async)"
|
||||
[label]="value"
|
||||
(click)="removeValue.emit(value)">
|
||||
</ds-sidebar-filter-selected-option>
|
||||
</ng-template>
|
||||
</div>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
12
src/app/shared/sidebar/filter/sidebar-filter.component.scss
Normal file
12
src/app/shared/sidebar/filter/sidebar-filter.component.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
:host .facet-filter {
|
||||
border: 1px solid map-get($theme-colors, light);
|
||||
cursor: pointer;
|
||||
|
||||
.sidebar-filter-wrapper.closed {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filter-toggle {
|
||||
line-height: $line-height-base;
|
||||
}
|
||||
}
|
89
src/app/shared/sidebar/filter/sidebar-filter.component.ts
Normal file
89
src/app/shared/sidebar/filter/sidebar-filter.component.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { SidebarFilterService } from './sidebar-filter.service';
|
||||
import { slide } from '../../animations/slide';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-sidebar-filter',
|
||||
styleUrls: ['./sidebar-filter.component.scss'],
|
||||
templateUrl: './sidebar-filter.component.html',
|
||||
animations: [slide],
|
||||
})
|
||||
/**
|
||||
* This components renders a sidebar filter including the label and the selected values.
|
||||
* The filter input itself should still be provided in the content.
|
||||
*/
|
||||
export class SidebarFilterComponent implements OnInit {
|
||||
|
||||
@Input() name:string;
|
||||
@Input() type:string;
|
||||
@Input() label:string;
|
||||
@Input() expanded = true;
|
||||
@Input() singleValue = false;
|
||||
@Input() selectedValues:Observable<string[]>;
|
||||
@Output() removeValue:EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* True when the filter is 100% collapsed in the UI
|
||||
*/
|
||||
closed = true;
|
||||
|
||||
/**
|
||||
* Emits true when the filter is currently collapsed in the store
|
||||
*/
|
||||
collapsed$:Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
protected filterService:SidebarFilterService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the state for this filter to collapsed when it's expanded and to expanded it when it's collapsed
|
||||
*/
|
||||
toggle() {
|
||||
this.filterService.toggle(this.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to change this.collapsed to false when the slide animation ends and is sliding open
|
||||
* @param event The animation event
|
||||
*/
|
||||
finishSlide(event:any):void {
|
||||
if (event.fromState === 'collapsed') {
|
||||
this.closed = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to change this.collapsed to true when the slide animation starts and is sliding closed
|
||||
* @param event The animation event
|
||||
*/
|
||||
startSlide(event:any):void {
|
||||
if (event.toState === 'collapsed') {
|
||||
this.closed = true;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit():void {
|
||||
this.closed = !this.expanded;
|
||||
this.initializeFilter();
|
||||
this.collapsed$ = this.isCollapsed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the initial state of the filter
|
||||
*/
|
||||
initializeFilter() {
|
||||
this.filterService.initializeFilter(this.name, this.expanded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the filter is currently collapsed
|
||||
* @returns {Observable<boolean>} Emits true when the current state of the filter is collapsed, false when it's expanded
|
||||
*/
|
||||
private isCollapsed():Observable<boolean> {
|
||||
return this.filterService.isCollapsed(this.name);
|
||||
}
|
||||
|
||||
}
|
70
src/app/shared/sidebar/filter/sidebar-filter.reducer.ts
Normal file
70
src/app/shared/sidebar/filter/sidebar-filter.reducer.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
FilterInitializeAction,
|
||||
SidebarFilterAction,
|
||||
SidebarFilterActionTypes
|
||||
} from './sidebar-filter.actions';
|
||||
|
||||
/**
|
||||
* Interface that represents the state for a single filters
|
||||
*/
|
||||
export interface SidebarFilterState {
|
||||
filterCollapsed:boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface that represents the state for all available filters
|
||||
*/
|
||||
export interface SidebarFiltersState {
|
||||
[name:string]:SidebarFilterState
|
||||
}
|
||||
|
||||
const initialState:SidebarFiltersState = Object.create(null);
|
||||
|
||||
/**
|
||||
* Performs a filter action on the current state
|
||||
* @param {SidebarFiltersState} state The state before the action is performed
|
||||
* @param {SidebarFilterAction} action The action that should be performed
|
||||
* @returns {SidebarFiltersState} The state after the action is performed
|
||||
*/
|
||||
export function sidebarFilterReducer(state = initialState, action:SidebarFilterAction):SidebarFiltersState {
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
case SidebarFilterActionTypes.INITIALIZE: {
|
||||
const initAction = (action as FilterInitializeAction);
|
||||
return Object.assign({}, state, {
|
||||
[action.filterName]: {
|
||||
filterCollapsed: !initAction.initiallyExpanded,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
case SidebarFilterActionTypes.COLLAPSE: {
|
||||
return Object.assign({}, state, {
|
||||
[action.filterName]: {
|
||||
filterCollapsed: true,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
case SidebarFilterActionTypes.EXPAND: {
|
||||
return Object.assign({}, state, {
|
||||
[action.filterName]: {
|
||||
filterCollapsed: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
case SidebarFilterActionTypes.TOGGLE: {
|
||||
return Object.assign({}, state, {
|
||||
[action.filterName]: {
|
||||
filterCollapsed: !state[action.filterName].filterCollapsed,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
90
src/app/shared/sidebar/filter/sidebar-filter.service.ts
Normal file
90
src/app/shared/sidebar/filter/sidebar-filter.service.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
FilterCollapseAction,
|
||||
FilterExpandAction, FilterInitializeAction,
|
||||
FilterToggleAction
|
||||
} from './sidebar-filter.actions';
|
||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { SidebarFiltersState, SidebarFilterState } from './sidebar-filter.reducer';
|
||||
import { Observable } from 'rxjs';
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { hasValue } from '../../empty.util';
|
||||
|
||||
/**
|
||||
* Service that performs all actions that have to do with sidebar filters like collapsing or expanding them.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SidebarFilterService {
|
||||
|
||||
constructor(private store:Store<SidebarFilterState>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an initialize action to the store for a given filter
|
||||
* @param {string} filter The filter for which the action is dispatched
|
||||
* @param {boolean} expanded If the filter should be open from the start
|
||||
*/
|
||||
public initializeFilter(filter:string, expanded:boolean):void {
|
||||
this.store.dispatch(new FilterInitializeAction(filter, expanded));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a collapse action to the store for a given filter
|
||||
* @param {string} filterName The filter for which the action is dispatched
|
||||
*/
|
||||
public collapse(filterName:string):void {
|
||||
this.store.dispatch(new FilterCollapseAction(filterName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an expand action to the store for a given filter
|
||||
* @param {string} filterName The filter for which the action is dispatched
|
||||
*/
|
||||
public expand(filterName:string):void {
|
||||
this.store.dispatch(new FilterExpandAction(filterName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a toggle action to the store for a given filter
|
||||
* @param {string} filterName The filter for which the action is dispatched
|
||||
*/
|
||||
public toggle(filterName:string):void {
|
||||
this.store.dispatch(new FilterToggleAction(filterName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the state of a given filter is currently collapsed or not
|
||||
* @param {string} filterName The filtername for which the collapsed state is checked
|
||||
* @returns {Observable<boolean>} Emits the current collapsed state of the given filter, if it's unavailable, return false
|
||||
*/
|
||||
isCollapsed(filterName:string):Observable<boolean> {
|
||||
return this.store.pipe(
|
||||
select(filterByNameSelector(filterName)),
|
||||
map((object:SidebarFilterState) => {
|
||||
if (object) {
|
||||
return object.filterCollapsed;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const filterStateSelector = (state:SidebarFiltersState) => state.sidebarFilter;
|
||||
|
||||
function filterByNameSelector(name:string):MemoizedSelector<SidebarFiltersState, SidebarFilterState> {
|
||||
return keySelector<SidebarFilterState>(name);
|
||||
}
|
||||
|
||||
export function keySelector<T>(key:string):MemoizedSelector<SidebarFiltersState, T> {
|
||||
return createSelector(filterStateSelector, (state:SidebarFilterState) => {
|
||||
if (hasValue(state)) {
|
||||
return state[key];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}
|
14
src/app/shared/sidebar/page-with-sidebar.component.html
Normal file
14
src/app/shared/sidebar/page-with-sidebar.component.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="row-with-sidebar row-offcanvas row-offcanvas-left"
|
||||
[@pushInOut]="(isSidebarCollapsed$ | async) ? 'collapsed' : 'expanded'">
|
||||
<div id="{{id}}-sidebar-content"
|
||||
class="col-12 col-md-{{sideBarWidth}} sidebar-content {{sidebarClasses | async}}">
|
||||
<ng-container *ngTemplateOutlet="sidebarContent"></ng-container>
|
||||
</div>
|
||||
<div class="col-12 col-md-{{12 - sideBarWidth}}">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
52
src/app/shared/sidebar/page-with-sidebar.component.scss
Normal file
52
src/app/shared/sidebar/page-with-sidebar.component.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
@include media-breakpoint-down(md) {
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.row-with-sidebar {
|
||||
|
||||
&.row-offcanvas {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
position: relative;
|
||||
|
||||
&.row-offcanvas {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&.row-offcanvas-right .sidebar-content {
|
||||
right: -100%;
|
||||
}
|
||||
|
||||
&.row-offcanvas-left .sidebar-content {
|
||||
left: -100%;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
.sidebar-content {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
z-index: $zindex-sticky;
|
||||
padding-top: $content-spacing;
|
||||
margin-top: -$content-spacing;
|
||||
align-self: flex-start;
|
||||
display: block;
|
||||
}
|
||||
}
|
75
src/app/shared/sidebar/page-with-sidebar.component.spec.ts
Normal file
75
src/app/shared/sidebar/page-with-sidebar.component.spec.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { PageWithSidebarComponent } from './page-with-sidebar.component';
|
||||
import { SidebarService } from './sidebar.service';
|
||||
import { HostWindowService } from '../host-window.service';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('PageWithSidebarComponent', () => {
|
||||
let comp:PageWithSidebarComponent;
|
||||
let fixture:ComponentFixture<PageWithSidebarComponent>;
|
||||
|
||||
const sidebarService = {
|
||||
isCollapsed: observableOf(true),
|
||||
collapse: () => this.isCollapsed = observableOf(true),
|
||||
expand: () => this.isCollapsed = observableOf(false)
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule],
|
||||
providers: [
|
||||
{
|
||||
provide: SidebarService,
|
||||
useValue: sidebarService
|
||||
},
|
||||
{
|
||||
provide: HostWindowService, useValue: jasmine.createSpyObj('hostWindowService',
|
||||
{
|
||||
isXs: observableOf(true),
|
||||
isSm: observableOf(false),
|
||||
isXsOrSm: observableOf(true)
|
||||
})
|
||||
},
|
||||
],
|
||||
declarations: [PageWithSidebarComponent]
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(PageWithSidebarComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.id = 'mock-id';
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe('when sidebarCollapsed is true in mobile view', () => {
|
||||
let menu:HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
menu = fixture.debugElement.query(By.css('#mock-id-sidebar-content')).nativeElement;
|
||||
(comp as any).sidebarService.isCollapsed = observableOf(true);
|
||||
comp.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should close the sidebar', () => {
|
||||
expect(menu.classList).not.toContain('active');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when sidebarCollapsed is false in mobile view', () => {
|
||||
let menu:HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
menu = fixture.debugElement.query(By.css('#mock-id-sidebar-content')).nativeElement;
|
||||
(comp as any).sidebarService.isCollapsed = observableOf(false);
|
||||
comp.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should open the menu', () => {
|
||||
expect(menu.classList).toContain('active');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
77
src/app/shared/sidebar/page-with-sidebar.component.ts
Normal file
77
src/app/shared/sidebar/page-with-sidebar.component.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Component, Input, OnInit, TemplateRef } from '@angular/core';
|
||||
import { SidebarService } from './sidebar.service';
|
||||
import { HostWindowService } from '../host-window.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { pushInOut } from '../animations/push';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-page-with-sidebar',
|
||||
styleUrls: ['./page-with-sidebar.component.scss'],
|
||||
templateUrl: './page-with-sidebar.component.html',
|
||||
animations: [pushInOut],
|
||||
})
|
||||
/**
|
||||
* This component takes care of displaying the sidebar properly on all viewports. It does not
|
||||
* provide default buttons to open or close the sidebar. Instead the parent component is expected
|
||||
* to provide the content of the sidebar through an input. The main content of the page goes in
|
||||
* the template outlet (inside the page-width-sidebar tags).
|
||||
*/
|
||||
export class PageWithSidebarComponent implements OnInit {
|
||||
@Input() id:string;
|
||||
@Input() sidebarContent:TemplateRef<any>;
|
||||
|
||||
/**
|
||||
* Emits true if were on a small screen
|
||||
*/
|
||||
isXsOrSm$:Observable<boolean>;
|
||||
|
||||
/**
|
||||
* The width of the sidebar (bootstrap columns)
|
||||
*/
|
||||
@Input()
|
||||
sideBarWidth = 3;
|
||||
|
||||
/**
|
||||
* Observable for whether or not the sidebar is currently collapsed
|
||||
*/
|
||||
isSidebarCollapsed$:Observable<boolean>;
|
||||
|
||||
sidebarClasses:Observable<string>;
|
||||
|
||||
constructor(protected sidebarService:SidebarService,
|
||||
protected windowService:HostWindowService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit():void {
|
||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||
this.isSidebarCollapsed$ = this.isSidebarCollapsed();
|
||||
this.sidebarClasses = this.isSidebarCollapsed$.pipe(
|
||||
map((isCollapsed) => isCollapsed ? '' : 'active')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the sidebar is collapsed
|
||||
* @returns {Observable<boolean>} emits true if the sidebar is currently collapsed, false if it is expanded
|
||||
*/
|
||||
private isSidebarCollapsed():Observable<boolean> {
|
||||
return this.sidebarService.isCollapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to a collapsed state
|
||||
*/
|
||||
public closeSidebar():void {
|
||||
this.sidebarService.collapse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to an expanded state
|
||||
*/
|
||||
public openSidebar():void {
|
||||
this.sidebarService.expand();
|
||||
}
|
||||
|
||||
}
|
@@ -3,24 +3,24 @@ import { Observable } from 'rxjs';
|
||||
import { provideMockActions } from '@ngrx/effects/testing';
|
||||
import { cold, hot } from 'jasmine-marbles';
|
||||
import * as fromRouter from '@ngrx/router-store';
|
||||
import { SearchSidebarCollapseAction } from './search-sidebar.actions';
|
||||
import { SearchSidebarEffects } from './search-sidebar.effects';
|
||||
import { SidebarCollapseAction } from './sidebar.actions';
|
||||
import { SidebarEffects } from './sidebar-effects.service';
|
||||
|
||||
describe('SearchSidebarEffects', () => {
|
||||
let sidebarEffects: SearchSidebarEffects;
|
||||
describe('SidebarEffects', () => {
|
||||
let sidebarEffects: SidebarEffects;
|
||||
let actions: Observable<any>;
|
||||
const dummyURL = 'http://f4fb15e2-1bd3-4e63-8d0d-486ad8bc714a';
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
SearchSidebarEffects,
|
||||
SidebarEffects,
|
||||
provideMockActions(() => actions),
|
||||
// other providers
|
||||
],
|
||||
});
|
||||
|
||||
sidebarEffects = TestBed.get(SearchSidebarEffects);
|
||||
sidebarEffects = TestBed.get(SidebarEffects);
|
||||
});
|
||||
|
||||
describe('routeChange$', () => {
|
||||
@@ -28,7 +28,7 @@ describe('SearchSidebarEffects', () => {
|
||||
it('should return a COLLAPSE action in response to an UPDATE_LOCATION action to a new route', () => {
|
||||
actions = hot('--a-', { a: { type: fromRouter.ROUTER_NAVIGATION, payload: {routerState: {url: dummyURL}} } });
|
||||
|
||||
const expected = cold('--b-', { b: new SearchSidebarCollapseAction() });
|
||||
const expected = cold('--b-', { b: new SidebarCollapseAction() });
|
||||
|
||||
expect(sidebarEffects.routeChange$).toBeObservable(expected);
|
||||
});
|
6
src/app/shared/sidebar/sidebar-dropdown.component.html
Normal file
6
src/app/shared/sidebar/sidebar-dropdown.component.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<div class="setting-option mb-3 p-3">
|
||||
<h5><label for="{{id}}">{{label | translate}}</label></h5>
|
||||
<select id="{{id}}" class="form-control" (change)="change.emit($event)">
|
||||
<ng-content></ng-content>
|
||||
</select>
|
||||
</div>
|
3
src/app/shared/sidebar/sidebar-dropdown.component.scss
Normal file
3
src/app/shared/sidebar/sidebar-dropdown.component.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.setting-option {
|
||||
border: 1px solid map-get($theme-colors, light);
|
||||
}
|
16
src/app/shared/sidebar/sidebar-dropdown.component.ts
Normal file
16
src/app/shared/sidebar/sidebar-dropdown.component.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-sidebar-dropdown',
|
||||
styleUrls: ['./sidebar-dropdown.component.scss'],
|
||||
templateUrl: './sidebar-dropdown.component.html',
|
||||
})
|
||||
/**
|
||||
* This components renders a sidebar dropdown including the label.
|
||||
* The options should still be provided in the content.
|
||||
*/
|
||||
export class SidebarDropdownComponent {
|
||||
@Input() id:string;
|
||||
@Input() label:string;
|
||||
@Output() change:EventEmitter<any> = new EventEmitter<number>();
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user