diff --git a/.gitignore b/.gitignore
index 0e550acbbb..b8dc2b176e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,7 +23,6 @@ speed-measure-plugin.json
*.launch
.settings/
*.sublime-workspace
-.history/*
# IDE - VSCode
.vscode/*
@@ -31,6 +30,7 @@ speed-measure-plugin.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
+.history/*
# misc
/.sass-cache
@@ -39,13 +39,13 @@ speed-measure-plugin.json
/typings
# logs
-logs
*.log
+logs
npm-debug.log*
+testem.log
yarn-debug.log*
yarn-error.log*
/libpeerconnection.log
-testem.log
# e2e
/e2e/*.js
diff --git a/.travis.yml b/.travis.yml
index 1254ad8058..7352f80905 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -19,7 +19,7 @@ cache:
before_install:
# install custom version of yarn & export executable
# see https://yarnpkg.com/en/docs/install-ci#travis-tab
- - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.15.2
+ - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.16.0
- export PATH=$HOME/.yarn/bin:$PATH
install:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c00114588..c84b64fe83 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,101 @@
-# Change Log
+# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+## [0.6.0](https://github.com/webern-unibas-ch/awg-app/compare/v0.5.4...v0.6.0) (2019-07-19)
+
+### Bug Fixes
+
+- **app:** add 404 fallback route ([c0ade9b](https://github.com/webern-unibas-ch/awg-app/commit/c0ade9b))
+- **app:** fix errors after update to Angular 8 ([2771649](https://github.com/webern-unibas-ch/awg-app/commit/2771649))
+- **app:** get correct section of meta data for remaining views ([80bbe10](https://github.com/webern-unibas-ch/awg-app/commit/80bbe10))
+- **app:** patch issue with CustomHammerConfig of ngx-gallery ([cb1a0d4](https://github.com/webern-unibas-ch/awg-app/commit/cb1a0d4))
+- **app:** remove temporary workaround with static ngInjectableDef ([962ffeb](https://github.com/webern-unibas-ch/awg-app/commit/962ffeb))
+- **app:** use onPush CD strategy on dumb components if possible ([f42f706](https://github.com/webern-unibas-ch/awg-app/commit/f42f706)), closes [#2](https://github.com/webern-unibas-ch/awg-app/issues/2)
+- **contact:** get correct section of meta data for contact view ([5d5a754](https://github.com/webern-unibas-ch/awg-app/commit/5d5a754))
+- **core:** fix missing space in copyright desc ([17f0865](https://github.com/webern-unibas-ch/awg-app/commit/17f0865))
+- **core:** fix visibility of centered navbar-brand ([c879bec](https://github.com/webern-unibas-ch/awg-app/commit/c879bec))
+- **core:** get only page meta data for footer and navbar ([2f51775](https://github.com/webern-unibas-ch/awg-app/commit/2f51775))
+- **core:** use better default values for clearing subjects ([9f65f13](https://github.com/webern-unibas-ch/awg-app/commit/9f65f13))
+- **core:** use MetaPerson class for authors and editors ([ff7df06](https://github.com/webern-unibas-ch/awg-app/commit/ff7df06))
+- **edition:** remove unnecessary toggle methods ([7f4b303](https://github.com/webern-unibas-ch/awg-app/commit/7f4b303))
+- **edition:** return only first emit of EditionDataService observables ([6e02f0b](https://github.com/webern-unibas-ch/awg-app/commit/6e02f0b))
+- **edition:** use async pipe for data in report component ([075a97e](https://github.com/webern-unibas-ch/awg-app/commit/075a97e))
+- **edition:** use CDS.onPush for report component's children ([a31483d](https://github.com/webern-unibas-ch/awg-app/commit/a31483d))
+- **search:** add bottom pagination in search result list ([6668f87](https://github.com/webern-unibas-ch/awg-app/commit/6668f87))
+- **search:** avoid bindings to pass static strings to native attributes ([945e259](https://github.com/webern-unibas-ch/awg-app/commit/945e259))
+- **search:** change snapshot path to get current url in search overview ([46995ff](https://github.com/webern-unibas-ch/awg-app/commit/46995ff))
+- **search:** clear search info on destroy of SearchResultListComponent ([c9ef240](https://github.com/webern-unibas-ch/awg-app/commit/c9ef240))
+- **search:** fix ngIfs in nested children templates of resource detail ([90d64ac](https://github.com/webern-unibas-ch/awg-app/commit/90d64ac))
+- **search:** improve communication btw overview and info ([357872e](https://github.com/webern-unibas-ch/awg-app/commit/357872e))
+- **search:** improve handling of resource detail requests ([431f9ab](https://github.com/webern-unibas-ch/awg-app/commit/431f9ab))
+- **search:** improve handling of searchResponseWithQuery subscription ([9be21c6](https://github.com/webern-unibas-ch/awg-app/commit/9be21c6))
+- **search:** improve linked objects component ([4e1347e](https://github.com/webern-unibas-ch/awg-app/commit/4e1347e))
+- **search:** improve search form building ([c2cab36](https://github.com/webern-unibas-ch/awg-app/commit/c2cab36))
+- **search:** move interceptor providers into separate file ([d9c7d2d](https://github.com/webern-unibas-ch/awg-app/commit/d9c7d2d))
+- **search:** move resource detail header out of tabset ([522c867](https://github.com/webern-unibas-ch/awg-app/commit/522c867))
+- **search:** provide loading status as observable ([ca41c8d](https://github.com/webern-unibas-ch/awg-app/commit/ca41c8d))
+- **search:** remove unused conversion service from resource detail ([5e377b3](https://github.com/webern-unibas-ch/awg-app/commit/5e377b3))
+- **search:** set search parameter nRows to 25 per default ([39b3f63](https://github.com/webern-unibas-ch/awg-app/commit/39b3f63))
+- **search:** simplify subscription to search result list data ([a9374d2](https://github.com/webern-unibas-ch/awg-app/commit/a9374d2))
+- **search:** subscribe to resource data instead of async pipe ([c8dab3f](https://github.com/webern-unibas-ch/awg-app/commit/c8dab3f))
+- **search:** use `this` instead of `super` in data api service ([42bac0a](https://github.com/webern-unibas-ch/awg-app/commit/42bac0a))
+- **search:** use async pipe for data in bibliography component ([05ff800](https://github.com/webern-unibas-ch/awg-app/commit/05ff800))
+- **search:** use async pipe for data in bibliography detail ([a95a45b](https://github.com/webern-unibas-ch/awg-app/commit/a95a45b))
+- **search:** use async pipe for data in resource detail ([7ae1372](https://github.com/webern-unibas-ch/awg-app/commit/7ae1372))
+- **search:** use enum for SearchParam view types ([7e53fa5](https://github.com/webern-unibas-ch/awg-app/commit/7e53fa5))
+- **search:** use getter for httpGetUrl in resourceDetail & searchPanel ([272d618](https://github.com/webern-unibas-ch/awg-app/commit/272d618))
+- **search:** use id tracker for search result list ([b9ebaa2](https://github.com/webern-unibas-ch/awg-app/commit/b9ebaa2))
+- **search:** use loading spinner for resource detail ([23b1272](https://github.com/webern-unibas-ch/awg-app/commit/23b1272)), closes [#5](https://github.com/webern-unibas-ch/awg-app/issues/5)
+- **search:** use SearchResponseWithQuery to update search params ([adb3d48](https://github.com/webern-unibas-ch/awg-app/commit/adb3d48))
+- **shared:** add optional 'toHtml' property to property json ([f7a3de5](https://github.com/webern-unibas-ch/awg-app/commit/f7a3de5))
+- **shared:** update compile html module & component ([73a9526](https://github.com/webern-unibas-ch/awg-app/commit/73a9526))
+- **side-info:** add getter/setter for osm urls in contact-info ([d578987](https://github.com/webern-unibas-ch/awg-app/commit/d578987))
+- **side-info:** get osm urls in contact-info from AppConfig ([f106c5f](https://github.com/webern-unibas-ch/awg-app/commit/f106c5f))
+- **side-info:** make address & osm map of contact-info shared components ([8846558](https://github.com/webern-unibas-ch/awg-app/commit/8846558))
+- **side-info:** make resource info data update immutable ([d248703](https://github.com/webern-unibas-ch/awg-app/commit/d248703))
+- **side-info:** remove nested subscription from resource-info ([779965f](https://github.com/webern-unibas-ch/awg-app/commit/779965f))
+- **side-info:** set edition info header from component ([15eb693](https://github.com/webern-unibas-ch/awg-app/commit/15eb693))
+- **side-info:** use async pipe for data in search info ([c7c9791](https://github.com/webern-unibas-ch/awg-app/commit/c7c9791))
+
+### Build System
+
+- **app:** add compodoc and format check scripts to package.json ([92a6852](https://github.com/webern-unibas-ch/awg-app/commit/92a6852))
+- **app:** add compodoc build to build scripts in package.json ([f3047b5](https://github.com/webern-unibas-ch/awg-app/commit/f3047b5))
+- **app:** configure karma.conf.js to run tests in order ([19cec04](https://github.com/webern-unibas-ch/awg-app/commit/19cec04))
+- **app:** remove core-js and update dependencies ([f25fcd3](https://github.com/webern-unibas-ch/awg-app/commit/f25fcd3))
+- **app:** update angular (^8.0.3) and cli (~8.0.6) ([001474e](https://github.com/webern-unibas-ch/awg-app/commit/001474e))
+- **app:** update dependencies after upgrade to Angular 8 ([0b00a91](https://github.com/webern-unibas-ch/awg-app/commit/0b00a91))
+- **app:** update dependency font-awesome ([bcc0f07](https://github.com/webern-unibas-ch/awg-app/commit/bcc0f07))
+- **app:** update remaining dependencies after upgrade to Angular 8 ([aeb5bfa](https://github.com/webern-unibas-ch/awg-app/commit/aeb5bfa))
+
+### Features
+
+- **app:** add compodoc for code documentation ([8945988](https://github.com/webern-unibas-ch/awg-app/commit/8945988))
+- **app:** update angular (^8.0.2) and cli (~8.0.3) ([3844e27](https://github.com/webern-unibas-ch/awg-app/commit/3844e27))
+- **contact:** add documentation section and link to Github repo ([3688358](https://github.com/webern-unibas-ch/awg-app/commit/3688358))
+- **core:** add loading interceptor to set load status ([7669bad](https://github.com/webern-unibas-ch/awg-app/commit/7669bad))
+- **core:** split meta object into sections and provide service method ([555fd11](https://github.com/webern-unibas-ch/awg-app/commit/555fd11))
+
+### Tests
+
+- **app:** fix broken tests - ongoing ([42d8a5d](https://github.com/webern-unibas-ch/awg-app/commit/42d8a5d))
+- **app:** fix broken tests after changes ([db2630f](https://github.com/webern-unibas-ch/awg-app/commit/db2630f))
+- **app:** fix tests with HttpTestingController ([6743f2c](https://github.com/webern-unibas-ch/awg-app/commit/6743f2c))
+- **app:** move nativeElement in own variable in tests ([70dba2d](https://github.com/webern-unibas-ch/awg-app/commit/70dba2d))
+- **core:** extend navbar tests ([6410bf8](https://github.com/webern-unibas-ch/awg-app/commit/6410bf8))
+- **home:** adjust tests for home-view component ([573ccd9](https://github.com/webern-unibas-ch/awg-app/commit/573ccd9))
+- **page-not-found:** fix broken tests after renaming of variables ([6db4d3c](https://github.com/webern-unibas-ch/awg-app/commit/6db4d3c))
+- **search:** add service method to searchResultList test after changes ([36512fb](https://github.com/webern-unibas-ch/awg-app/commit/36512fb))
+- **search:** add TwelveToneSpinnerStub to resource detail test ([0dd7ee2](https://github.com/webern-unibas-ch/awg-app/commit/0dd7ee2))
+- **search:** fix broken tests after switch to CD.OnPush - ongoing ([cb8a968](https://github.com/webern-unibas-ch/awg-app/commit/cb8a968))
+- **search:** fix broken tests after switch to CD.OnPush - ongoing ([01e7fdc](https://github.com/webern-unibas-ch/awg-app/commit/01e7fdc))
+- **search:** use SearchPramsViewTypes in search result list ([7793172](https://github.com/webern-unibas-ch/awg-app/commit/7793172))
+- **side-info:** add header test for structure-info ([4a18b67](https://github.com/webern-unibas-ch/awg-app/commit/4a18b67))
+- **side-info:** add test for contact-info and its child components ([48434d6](https://github.com/webern-unibas-ch/awg-app/commit/48434d6))
+- **side-info:** add tests for edition-info component ([b08216c](https://github.com/webern-unibas-ch/awg-app/commit/b08216c))
+- **side-info:** add tests for structure info component ([1a8bbaf](https://github.com/webern-unibas-ch/awg-app/commit/1a8bbaf))
+
## [0.5.4](https://github.com/webern-unibas-ch/awg-app/compare/v0.5.3...v0.5.4) (2019-04-09)
### Bug Fixes
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..61fd733aa3
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+- Using welcoming and inclusive language
+- Being respectful of differing viewpoints and experiences
+- Gracefully accepting constructive criticism
+- Focusing on what is best for the community
+- Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+- The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+- Trolling, insulting/derogatory comments, and personal or political attacks
+- Public or private harassment
+- Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+- Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies within all project spaces, and it also applies when
+an individual is representing the project or its community in public spaces.
+Examples of representing a project or community include using an official
+project e-mail address, posting via an official social media account, or acting
+as an appointed representative at an online or offline event. Representation of
+a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at `info-awg (at) unibas.ch`. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000..32afd1b26c
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,125 @@
+# Contributing
+
+When contributing to this repository, please first discuss the change you wish to make via issue,
+email, or any other method with the owners of this repository before making a change.
+
+Please note that this project is released with a [Code of Conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project.
+
+## Table of Contents
+
+- [Contribution process](#contribution-process)
+ - [Branching / Git flow](#branching--git-flow)
+ - [Commit Message Schema](#commit-message-schema)
+ - [Release Versioning Convention](#release-versioning-convention)
+- [Angular quick start guide](#quick-start-guide)
+ - [Prerequisites](#prerequisites)
+ - [Development server](#development-server)
+ - [Code scaffolding](#code-scaffolding)
+ - [Build](#build)
+ - [Running unit tests](#running-unit-tests)
+ - [Running end-to-end tests](#running-end-to-end-tests)
+ - [Further help](#further-help)
+- [Code of Conduct](#code-of-conduct)
+
+## Contribution process
+
+### Branching / Git flow
+
+This project uses the [Gitflow Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) which defines a strict branching model designed around the project releases. Therefore the following branch structure is used
+
+- `master` (stores the official release history; all commits are tagged with a version number)
+- `develop` (serves as an integration branch for features; gets released into `release/xxx`)
+- `feature/XXX` (main branch for developing new features; gets branched from and merged into `develop`, never interacts with `master`)
+- `release/xxx` (used to prepare a release with latest features from `develop`; gets merged into `master`)
+- `hotfix/xxx` (used to quickly patch production releases; forked from and merged directly into `master`)
+
+To initialize the GitFlow workflow execute `git flow init` inside your local copy of the repository.
+
+To provide a new feature or changes to the code, create a new feature branch from develop and make a pull request when ready. Keep care of the [Commit Message Schema](#commit-message-schema) described below.
+
+For more information about pull requests go check out the GitHub Help [About pull requests](https://help.github.com/en/articles/about-pull-requests).
+
+### Commit Message Schema
+
+This project follows the [Conventional Commits Specification](https://conventionalcommits.org) using [commitlint](https://conventional-changelog.github.io/commitlint/#/) based on the [Angular configuration](https://github.com/conventional-changelog/commitlint/tree/master/@commitlint/config-angular) (further explanation can be found in the [Angular commit-message-guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines)).
+
+Using these conventions leads to more readable messages that are easy to follow when looking through the project history. But also, we use the git commit messages to autogenerate the [CHANGELOG](https://github.com/webern-unibas-ch/awg-app/blob/master/LICENSE.md) and automate versions by means of [standard-version](https://github.com/conventional-changelog/standard-version) (see "Release Versioning Convention" section below).
+
+When writing commit messages, we stick to this schema:
+
+```
+():
+
+
+
+
+```
+
+The **header** is mandatory, and **type** and **scope** of the header must be one of the following:
+
+Types:
+
+- `build` (changes that affect the build system or external dependencies; no production code changes),
+- `ci` (changes to Continuous Integration, no production code changes)
+- `docs` (changes to the documentation, no production code changes),
+- `feat` (new feature for the user),
+- `fix` (bug fix for the user),
+- `perf` (code change that improves performance),
+- `refactor` (refactoring production code, eg. renaming a variable),
+- `revert` (reverting a former commit),
+- `style` (formatting, etc; no production code changes),
+- `test` (adding missing tests, refactoring tests; no production code changes)
+
+Scopes (specific to this project, not part of the Angular convention):
+
+- `app`
+- `home`
+- `edition`
+- `search`
+- `structure`
+- `contact`
+
+Example:
+
+```
+feat(edition): add route for resource creation
+
+- add path for multipart request
+- adapt handling of resources responder
+```
+
+### Release Versioning Convention
+
+We use the git commit messages to autogenerate the [CHANGELOG](https://github.com/webern-unibas-ch/awg-app/blob/master/CHANGELOG.md) and automate versions by means of [standard-version](https://github.com/conventional-changelog/standard-version).
+
+## Angular quick start guide
+
+This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.0.3. Check there for necessary prerequisites (which comprise `Node` and `npm`or `yarn`).
+
+### Development server
+
+Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
+
+### Code scaffolding
+
+Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
+
+### Build
+
+Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
+
+### Running unit tests
+
+Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
+
+### Running end-to-end tests
+
+Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
+
+### Further help
+
+To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
+
+## Code of Conduct
+
+See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
diff --git a/README.md b/README.md
index 34f51d88eb..0c8b71a2bb 100644
--- a/README.md
+++ b/README.md
@@ -3,125 +3,15 @@

[](https://travis-ci.org/webern-unibas-ch/awg-app)
[](https://codecov.io/gh/webern-unibas-ch/awg-app)
+[](https://edition.anton-webern.ch/compodoc/index.html)
[](https://github.com/prettier/prettier)
+[](code-of-conduct.md)
A prototype web application for the online edition of the [Anton Webern Gesamtausgabe](https://www.anton-webern.ch), located at the Musicological Seminar of the University of Basel. It is written in [Angular](https://angular.io/) and runs on [edition.anton-webern.ch](https://edition.anton-webern.ch).
-## Table of Contents
-
-- [Contributing](#contributing)
- - [Quick start guide](#quick-start-guide)
- - [Prerequisites](#prerequisites)
- - [Development server](#development-server)
- - [Code scaffolding](#code-scaffolding)
- - [Build](#build)
- - [Running unit tests](#running-unit-tests)
- - [Running end-to-end tests](#running-end-to-end-tests)
- - [Further help](#further-help)
- - [Branching / Git flow](#branching--git-flow)
- - [Commit Message Schema](#commit-message-schema)
- - [Release Versioning Convention](#release-versioning-convention)
-- [License](#license)
-
## Contributing
-## Quick start guide
-
-### Prerequisites
-
-This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.3.6 (for installation instructions see the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/packages/angular/cli/README.md)).
-
-Both the Angular CLI and generated project have dependencies that require Node 8.9 or higher, together
-with NPM 5.5.1 or higher.
-
-### Development server
-
-Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
-
-### Code scaffolding
-
-Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
-
-### Build
-
-Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
-
-### Running unit tests
-
-Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
-
-### Running end-to-end tests
-
-Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
-
-### Further help
-
-To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
-
-## Branching / Git flow
-
-This project uses the [Gitflow Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) which defines a strict branching model designed around the project release. Therefore the following branch structure is used
-
-- `master` (stores the official release history; all commits are tagged with a version number)
-- `develop` (serves as an integration branch for features; gets released into `release/xxx`)
-- `feature/XXX` (main branch for developing new features; gets merged into `develop`, never interacts with `master`)
-- `release/xxx` (used to prepare a release with latest features from `develop`; gets merged into `master`)
-- `hotfix/xxx` (used to quickly patch production releases; forked from and merged directly into `master`)
-
-To initialize the GitFlow workflow execute `git flow init` inside your local copy of the repository.
-
-## Commit Message Schema
-
-This project follows the [Conventional Commits Specification](https://conventionalcommits.org) using [commitlint](https://conventional-changelog.github.io/commitlint/#/) based on the [Angular configuration](https://github.com/conventional-changelog/commitlint/tree/master/@commitlint/config-angular) (further explanation can be found in the [Angular commit-message-guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines)).
-
-Using these conventions leads to more readable messages that are easy to follow when looking through the project history. But also, we use the git commit messages to autogenerate the [CHANGELOG](https://github.com/webern-unibas-ch/awg-app/blob/master/LICENSE.md) and automate versions by means of [standard-version](https://github.com/conventional-changelog/standard-version) (see "Release Versioning Convention" section below).
-
-When writing commit messages, we stick to this schema:
-
-```
-():
-
-
-
-
- Version {{ metaData?.page?.version }} ◇
- {{ metaData?.page?.versionReleaseDate }}
+ Version {{ pageMetaData?.version }} ◇
+ {{ pageMetaData?.versionReleaseDate }}
-
+
Impressum
+ |
+
+ Dokumentation
+
diff --git a/src/app/core/footer/footer-declaration/footer-declaration.component.spec.ts b/src/app/core/footer/footer-declaration/footer-declaration.component.spec.ts
index ac42153b86..32052ce05b 100644
--- a/src/app/core/footer/footer-declaration/footer-declaration.component.spec.ts
+++ b/src/app/core/footer/footer-declaration/footer-declaration.component.spec.ts
@@ -5,7 +5,7 @@ import { click } from '@testing/click-helper';
import { getAndExpectDebugElementByCss, getAndExpectDebugElementByDirective } from '@testing/expect-helper';
import { RouterLinkStubDirective } from '@testing/router-stubs';
-import { Meta } from '@awg-core/core-models';
+import { MetaPage, MetaSectionTypes } from '@awg-core/core-models';
import { METADATA } from '@awg-core/mock-data';
import { FooterDeclarationComponent } from './footer-declaration.component';
@@ -18,7 +18,7 @@ describe('FooterDeclarationComponent (DONE)', () => {
let linkDes: DebugElement[];
let routerLinks;
- let expectedMetaData: Meta;
+ let expectedPageMetaData: MetaPage;
beforeEach(async(() => {
TestBed.configureTestingModule({
@@ -33,7 +33,7 @@ describe('FooterDeclarationComponent (DONE)', () => {
compEl = compDe.nativeElement;
// test data
- expectedMetaData = METADATA;
+ expectedPageMetaData = METADATA[MetaSectionTypes.page];
});
it('should create', () => {
@@ -41,8 +41,8 @@ describe('FooterDeclarationComponent (DONE)', () => {
});
describe('BEFORE initial data binding', () => {
- it('should not have metaData', () => {
- expect(component.metaData).toBeUndefined('should be undefined');
+ it('should not have pageMetaData', () => {
+ expect(component.pageMetaData).toBeUndefined('should be undefined');
});
describe('VIEW', () => {
@@ -50,7 +50,7 @@ describe('FooterDeclarationComponent (DONE)', () => {
getAndExpectDebugElementByCss(compDe, 'p', 3, 3);
});
- it('... should not render metaData yet', () => {
+ it('... should not render pageMetaData yet', () => {
// find debug elements
const versionDes = getAndExpectDebugElementByCss(compDe, '#awg-version', 1, 1);
const versionDateDes = getAndExpectDebugElementByCss(compDe, '#awg-version-date', 1, 1);
@@ -72,7 +72,7 @@ describe('FooterDeclarationComponent (DONE)', () => {
describe('AFTER initial data binding', () => {
beforeEach(() => {
// simulate the parent setting the input properties
- component.metaData = expectedMetaData;
+ component.pageMetaData = expectedPageMetaData;
// trigger initial data binding
fixture.detectChanges();
@@ -80,8 +80,8 @@ describe('FooterDeclarationComponent (DONE)', () => {
describe('VIEW', () => {
it('should render values', () => {
- const expectedVersion = expectedMetaData.page.version;
- const expectedVersionDate = expectedMetaData.page.versionReleaseDate;
+ const expectedVersion = expectedPageMetaData.version;
+ const expectedVersionDate = expectedPageMetaData.versionReleaseDate;
// find debug elements
const versionDes = getAndExpectDebugElementByCss(compDe, '#awg-version', 1, 1);
@@ -102,27 +102,40 @@ describe('FooterDeclarationComponent (DONE)', () => {
describe('[routerLink]', () => {
beforeEach(() => {
// find DebugElements with an attached RouterLinkStubDirective
- linkDes = getAndExpectDebugElementByDirective(compDe, RouterLinkStubDirective, 1, 1);
+ linkDes = getAndExpectDebugElementByDirective(compDe, RouterLinkStubDirective, 2, 2);
// get attached link directive instances using each DebugElement's injector
routerLinks = linkDes.map(de => de.injector.get(RouterLinkStubDirective));
});
- it('... can get routerLink from template', () => {
- expect(routerLinks.length).toBe(1, 'should have 1 routerLink');
+ it('... can get routerLinks from template', () => {
+ expect(routerLinks.length).toBe(2, 'should have 2 routerLinks');
expect(routerLinks[0].linkParams).toEqual(['/contact']);
+ expect(routerLinks[1].linkParams).toEqual(['/contact']);
});
- it('... can click Contact link in template', () => {
- const contactLinkDe = linkDes[0]; // contact link DebugElement
- const contactLink = routerLinks[0]; // contact link directive
+ it('... can click imprint link in template', () => {
+ const imprintLinkDe = linkDes[0]; // contact link DebugElement
+ const imprintLink = routerLinks[0]; // contact link directive
- expect(contactLink.navigatedTo).toBeNull('should not have navigated yet');
+ expect(imprintLink.navigatedTo).toBeNull('should not have navigated yet');
- click(contactLinkDe);
+ click(imprintLinkDe);
fixture.detectChanges();
- expect(contactLink.navigatedTo).toEqual(['/contact']);
+ expect(imprintLink.navigatedTo).toEqual(['/contact']);
+ });
+
+ it('... can click documentation link in template', () => {
+ const documentationLinkDe = linkDes[1]; // contact link DebugElement
+ const documentationLink = routerLinks[1]; // contact link directive
+
+ expect(documentationLink.navigatedTo).toBeNull('should not have navigated yet');
+
+ click(documentationLinkDe);
+ fixture.detectChanges();
+
+ expect(documentationLink.navigatedTo).toEqual(['/contact']);
});
});
});
diff --git a/src/app/core/footer/footer-declaration/footer-declaration.component.ts b/src/app/core/footer/footer-declaration/footer-declaration.component.ts
index 22bc88bd1e..1c4cf2dfc9 100644
--- a/src/app/core/footer/footer-declaration/footer-declaration.component.ts
+++ b/src/app/core/footer/footer-declaration/footer-declaration.component.ts
@@ -1,17 +1,25 @@
-import { Component, Input, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
-import { Meta } from '@awg-core/core-models';
+import { MetaPage } from '@awg-core/core-models';
+/**
+ * The FooterDeclaration component.
+ *
+ * It contains the declaration section of the footer
+ * with version number, release date and impressum.
+ */
@Component({
selector: 'awg-footer-declaration',
templateUrl: './footer-declaration.component.html',
- styleUrls: ['./footer-declaration.component.css']
+ styleUrls: ['./footer-declaration.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
-export class FooterDeclarationComponent implements OnInit {
+export class FooterDeclarationComponent {
+ /**
+ * Input variable: pageMetaData.
+ *
+ * It keeps the page meta data for the component.
+ */
@Input()
- metaData: Meta;
-
- constructor() {}
-
- ngOnInit() {}
+ pageMetaData: MetaPage;
}
diff --git a/src/app/core/footer/footer-logo/footer-logo.component.ts b/src/app/core/footer/footer-logo/footer-logo.component.ts
index f1e6af19d4..2cdc776fb4 100644
--- a/src/app/core/footer/footer-logo/footer-logo.component.ts
+++ b/src/app/core/footer/footer-logo/footer-logo.component.ts
@@ -1,17 +1,24 @@
-import { Component, Input, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Logo } from '@awg-core/core-models';
+/**
+ * The FooterLogo component.
+ *
+ * It contains a footer logo.
+ */
@Component({
selector: 'awg-footer-logo',
templateUrl: './footer-logo.component.html',
- styleUrls: ['./footer-logo.component.css']
+ styleUrls: ['./footer-logo.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
-export class FooterLogoComponent implements OnInit {
+export class FooterLogoComponent {
+ /**
+ * Input variable: logo.
+ *
+ * It keeps the logo data for the component.
+ */
@Input()
logo: Logo;
-
- constructor() {}
-
- ngOnInit() {}
}
diff --git a/src/app/core/footer/footer-poweredby/footer-poweredby.component.html b/src/app/core/footer/footer-poweredby/footer-poweredby.component.html
index b904c532b9..1167af038f 100644
--- a/src/app/core/footer/footer-poweredby/footer-poweredby.component.html
+++ b/src/app/core/footer/footer-poweredby/footer-poweredby.component.html
@@ -1,3 +1,6 @@
- Built with
and
+ Built on
+
+ with
and
+
diff --git a/src/app/core/footer/footer-poweredby/footer-poweredby.component.spec.ts b/src/app/core/footer/footer-poweredby/footer-poweredby.component.spec.ts
index 76754ee769..cfde76dfdd 100644
--- a/src/app/core/footer/footer-poweredby/footer-poweredby.component.spec.ts
+++ b/src/app/core/footer/footer-poweredby/footer-poweredby.component.spec.ts
@@ -35,18 +35,6 @@ describe('FooterPoweredbyComponent (DONE)', () => {
// test data
expectedLogos = {
- unibas: {
- id: 'unibaslogo',
- src: 'assets/img/logos/uni.svg',
- alt: 'Logo Uni Basel',
- href: 'https://www.unibas.ch'
- },
- snf: {
- id: 'snflogo',
- src: 'assets/img/logos/snf.png',
- alt: 'Logo SNF',
- href: 'http://www.snf.ch'
- },
angular: {
id: 'angularlogo',
src: 'assets/img/logos/angular.svg',
@@ -58,6 +46,24 @@ describe('FooterPoweredbyComponent (DONE)', () => {
src: 'assets/img/logos/ng-bootstrap.svg',
alt: 'Logo ng-Bootstrap',
href: 'https://ng-bootstrap.github.io/'
+ },
+ github: {
+ id: 'githublogo',
+ src: 'assets/img/logos/github.svg',
+ alt: 'Logo GitHub',
+ href: 'https://github.com/webern-unibas-ch/awg-app'
+ },
+ snf: {
+ id: 'snflogo',
+ src: 'assets/img/logos/snf.png',
+ alt: 'Logo SNF',
+ href: 'http://www.snf.ch'
+ },
+ unibas: {
+ id: 'unibaslogo',
+ src: 'assets/img/logos/uni.svg',
+ alt: 'Logo Uni Basel',
+ href: 'https://www.unibas.ch'
}
};
});
@@ -76,8 +82,8 @@ describe('FooterPoweredbyComponent (DONE)', () => {
getAndExpectDebugElementByCss(compDe, 'div.awg-powered-by', 1, 1);
});
- it('... should contain 2 footer logo components (stubbed)', () => {
- getAndExpectDebugElementByDirective(compDe, FooterLogoStubComponent, 2, 2);
+ it('... should contain 3 footer logo components (stubbed)', () => {
+ getAndExpectDebugElementByDirective(compDe, FooterLogoStubComponent, 3, 3);
});
});
});
@@ -91,25 +97,28 @@ describe('FooterPoweredbyComponent (DONE)', () => {
fixture.detectChanges();
});
- it('should have logo', () => {
+ it('should have logos', () => {
expect(component.logos).toBeDefined();
expect(component.logos).toBe(expectedLogos);
});
describe('VIEW', () => {
it('... should pass down logos to footer logo components', () => {
- const footerLogoDes = getAndExpectDebugElementByDirective(compDe, FooterLogoStubComponent, 2, 2);
+ const footerLogoDes = getAndExpectDebugElementByDirective(compDe, FooterLogoStubComponent, 3, 3);
const footerLogoCmps = footerLogoDes.map(
de => de.injector.get(FooterLogoStubComponent) as FooterLogoStubComponent
);
- expect(footerLogoCmps.length).toBe(2, 'should have 2 logo components');
+ expect(footerLogoCmps.length).toBe(3, 'should have 3 logo components');
expect(footerLogoCmps[0].logo).toBeTruthy();
- expect(footerLogoCmps[0].logo).toEqual(expectedLogos.angular, 'should have angular logo');
+ expect(footerLogoCmps[0].logo).toEqual(expectedLogos.github, 'should have github logo');
expect(footerLogoCmps[1].logo).toBeTruthy();
- expect(footerLogoCmps[1].logo).toEqual(expectedLogos.bootstrap, 'should have bootstrap logo');
+ expect(footerLogoCmps[1].logo).toEqual(expectedLogos.angular, 'should have angular logo');
+
+ expect(footerLogoCmps[2].logo).toBeTruthy();
+ expect(footerLogoCmps[2].logo).toEqual(expectedLogos.bootstrap, 'should have bootstrap logo');
});
});
});
diff --git a/src/app/core/footer/footer-poweredby/footer-poweredby.component.ts b/src/app/core/footer/footer-poweredby/footer-poweredby.component.ts
index ee2c7ded99..25171fab7e 100644
--- a/src/app/core/footer/footer-poweredby/footer-poweredby.component.ts
+++ b/src/app/core/footer/footer-poweredby/footer-poweredby.component.ts
@@ -1,17 +1,24 @@
-import { Component, Input, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Logos } from '@awg-core/core-models';
+/**
+ * The FooterPoweredBy component.
+ *
+ * It contains the poweredby section of the footer.
+ */
@Component({
selector: 'awg-footer-poweredby',
templateUrl: './footer-poweredby.component.html',
- styleUrls: ['./footer-poweredby.component.css']
+ styleUrls: ['./footer-poweredby.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
-export class FooterPoweredbyComponent implements OnInit {
+export class FooterPoweredbyComponent {
+ /**
+ * Input variable: logos.
+ *
+ * It keeps the logos data for the component.
+ */
@Input()
logos: Logos;
-
- constructor() {}
-
- ngOnInit() {}
}
diff --git a/src/app/core/footer/footer.component.html b/src/app/core/footer/footer.component.html
index cabfa8ab8c..39efd46b5b 100644
--- a/src/app/core/footer/footer.component.html
+++ b/src/app/core/footer/footer.component.html
@@ -2,7 +2,7 @@
+
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/linkedobjects/linkedobjects.component.spec.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/linkedobjects/linkedobjects.component.spec.ts
index 3602de25c0..92d015101a 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/linkedobjects/linkedobjects.component.spec.ts
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/linkedobjects/linkedobjects.component.spec.ts
@@ -1,14 +1,13 @@
import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
-import { DebugElement, SimpleChange, ɵdefaultKeyValueDiffers as defaultKeyValueDiffers } from '@angular/core';
-import { KeyValuePipe } from '@angular/common';
+import { DebugElement } from '@angular/core';
import Spy = jasmine.Spy;
import { click, clickAndAwaitChanges } from '@testing/click-helper';
import { expectSpyCall, getAndExpectDebugElementByCss } from '@testing/expect-helper';
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
-import { ConversionService } from '@awg-core/services';
-import { ResourceDetailGroupedIncomingLinks, ResourceDetailIncomingLinks } from '@awg-views/data-view/models';
+
+import { ResourceDetailGroupedIncomingLinks, ResourceDetailIncomingLink } from '@awg-views/data-view/models';
import { ResourceDetailHtmlContentLinkedobjectsComponent } from './linkedobjects.component';
@@ -28,35 +27,20 @@ describe('ResourceDetailHtmlContentLinkedobjectsComponent (DONE)', () => {
let compDe: DebugElement;
let compEl: any;
- let keyValuePipe: KeyValuePipe;
-
- let updateTotalNumberSpy: Spy;
+ let totalNumberSpy: Spy;
let navigateToResourceSpy: Spy;
let emitSpy: Spy;
- let expectedIncoming: ResourceDetailGroupedIncomingLinks;
- let incomingLink1: ResourceDetailIncomingLinks;
- let incomingLink2: ResourceDetailIncomingLinks;
- let incomingLink3: ResourceDetailIncomingLinks;
+ let expectedIncoming: ResourceDetailGroupedIncomingLinks[];
+ let incomingLink1: ResourceDetailIncomingLink;
+ let incomingLink2: ResourceDetailIncomingLink;
+ let incomingLink3: ResourceDetailIncomingLink;
const expectedTotalItems = 5;
- const mockConversionService = {
- getNestedArraysTotalItems: (obj: ResourceDetailGroupedIncomingLinks): number => {
- let size = 0;
- // iterate over object keys
- Object.keys(obj).forEach(key => {
- // sum up length of array nested in object
- size += obj[key].length;
- });
- return size;
- }
- };
-
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [NgbAccordionModule],
- declarations: [ResourceDetailHtmlContentLinkedobjectsComponent],
- providers: [{ provide: ConversionService, useValue: mockConversionService }]
+ declarations: [ResourceDetailHtmlContentLinkedobjectsComponent]
}).compileComponents();
}));
@@ -69,7 +53,7 @@ describe('ResourceDetailHtmlContentLinkedobjectsComponent (DONE)', () => {
// test data
incomingLink1 = {
id: '',
- value: 'testvalue1',
+ value: 'testexpectedLinkValue1',
restype: { id: '1234', label: 'test-type1', icon: '/assets/img/logos/angular.png' }
};
incomingLink2 = {
@@ -83,19 +67,18 @@ describe('ResourceDetailHtmlContentLinkedobjectsComponent (DONE)', () => {
restype: { id: '1236', label: 'test-type3', icon: '/assets/img/logos/awg.png' }
};
- expectedIncoming = new ResourceDetailGroupedIncomingLinks();
- expectedIncoming = {
- testkey1: [incomingLink1, incomingLink2],
- testkey2: [incomingLink1, incomingLink2, incomingLink3]
- };
-
- // set mockService to return default value
- mockConversionService.getNestedArraysTotalItems(expectedIncoming);
+ expectedIncoming = [
+ {
+ restypeLabel: 'testkey1',
+ links: [incomingLink1, incomingLink2]
+ },
+ { restypeLabel: 'testkey2', links: [incomingLink1, incomingLink2, incomingLink3] }
+ ];
// spies on component functions
// `.and.callThrough` will track the spy down the nested describes, see
// https://jasmine.github.io/2.0/introduction.html#section-Spies:_%3Ccode%3Eand.callThrough%3C/code%3E
- updateTotalNumberSpy = spyOn(component, 'updateTotalNumber').and.callThrough();
+ totalNumberSpy = spyOnProperty(component, 'totalNumber', 'get').and.callThrough();
navigateToResourceSpy = spyOn(component, 'navigateToResource').and.callThrough();
emitSpy = spyOn(component.resourceRequest, 'emit').and.callThrough();
});
@@ -105,12 +88,13 @@ describe('ResourceDetailHtmlContentLinkedobjectsComponent (DONE)', () => {
});
describe('BEFORE initial data binding', () => {
- it('should have totalNumber = 0', () => {
- expect(component.totalNumber).toBe(0, 'should be 0');
+ it('should not have `incomingGroups` inputs', () => {
+ expect(component.incomingGroups).toBeUndefined('should be undefined');
});
- it('should not have `incoming` inputs', () => {
- expect(component.incoming).toBeUndefined('should be undefined');
+ it('should have totalNumber = 0', () => {
+ expect(component.totalNumber).toBeDefined();
+ expect(component.totalNumber).toBe(0, 'should be 0');
});
describe('VIEW', () => {
@@ -125,7 +109,7 @@ describe('ResourceDetailHtmlContentLinkedobjectsComponent (DONE)', () => {
getAndExpectDebugElementByCss(headerDes[0], 'span#awg-incoming-size', 0, 0);
});
- it('... should contain one ngb-accordion (empty yet))', () => {
+ it('... should contain one ngb-accordion without panels yet', () => {
// ngb-accordion debug element
const accordionDes = getAndExpectDebugElementByCss(compDe, 'div.awg-linked-obj > ngb-accordion', 1, 1);
@@ -134,12 +118,6 @@ describe('ResourceDetailHtmlContentLinkedobjectsComponent (DONE)', () => {
});
});
- describe('#updateTotalNumber', () => {
- it('... should not have been called', () => {
- expect(updateTotalNumberSpy).not.toHaveBeenCalled();
- });
- });
-
describe('#navigateToResource', () => {
it('... should not have been called', () => {
expect(navigateToResourceSpy).not.toHaveBeenCalled();
@@ -151,17 +129,46 @@ describe('ResourceDetailHtmlContentLinkedobjectsComponent (DONE)', () => {
describe('AFTER initial data binding', () => {
beforeEach(() => {
// simulate the parent setting the input properties
- component.incoming = expectedIncoming;
+ component.incomingGroups = expectedIncoming;
// trigger initial data binding
fixture.detectChanges();
});
- it('should have `incoming` inputs', () => {
- expect(component.incoming).toBeDefined('should be defined');
- expect(component.incoming).toBe(expectedIncoming, `should be expectedIncoming: ${expectedIncoming}`);
+ it('should have `incomingGroups` inputs', () => {
+ expect(component.incomingGroups).toBeDefined('should be defined');
+ expect(component.incomingGroups).toBe(expectedIncoming, `should be expectedIncoming: ${expectedIncoming}`);
});
+ it('... should have updated totalNumber with number of nested items in `incomingGroups`', fakeAsync(() => {
+ expectSpyCall(totalNumberSpy, 1);
+
+ expect(component.totalNumber).toBe(
+ expectedTotalItems,
+ `should be expectedTotalItems: ${expectedTotalItems}`
+ );
+ }));
+
+ it('... should recalculate total number on input changes (second & more)', fakeAsync(() => {
+ const newExpectedIncomingGroups: ResourceDetailGroupedIncomingLinks[] = [
+ {
+ restypeLabel: 'testkey1',
+ links: [incomingLink1, incomingLink2, incomingLink3, incomingLink1]
+ }
+ ];
+ const newExpectedTotalItems = newExpectedIncomingGroups[0].links.length;
+
+ // simulate the parent changing the input properties
+ // no fixture detect changes needed because totalNumber is a getter()
+ component.incomingGroups = newExpectedIncomingGroups;
+
+ // output has changed
+ expect(component.totalNumber).toBe(
+ newExpectedTotalItems,
+ `should be newExpectedTotalItems: ${newExpectedTotalItems}`
+ );
+ }));
+
describe('VIEW', () => {
it('... should contain one header showing number of items', () => {
// size debug elements
@@ -176,8 +183,8 @@ describe('ResourceDetailHtmlContentLinkedobjectsComponent (DONE)', () => {
// check size output
expect(sizeEl.textContent).toBeDefined();
expect(sizeEl.textContent).toContain(
- expectedTotalItems.toString(),
- `should contain expectedTotalItems: ${expectedTotalItems}`
+ component.totalNumber,
+ `should contain expectedTotalItems: ${component.totalNumber}`
);
});
@@ -206,11 +213,9 @@ describe('ResourceDetailHtmlContentLinkedobjectsComponent (DONE)', () => {
);
});
- it('... should render incoming group length and key in panel header (div.card-header)', () => {
+ it('... should render incoming group length as badges in panel header (div.card-header)', () => {
// header debug element
const panelHeaderDes = getAndExpectDebugElementByCss(compDe, 'div.card > div.card-header', 2, 2);
-
- // badge debug elements
const badgeDes0 = getAndExpectDebugElementByCss(
panelHeaderDes[0],
'span.badge',
@@ -226,19 +231,32 @@ describe('ResourceDetailHtmlContentLinkedobjectsComponent (DONE)', () => {
'in second panel'
);
- // badge native elements
const badge0El = badgeDes0[0].nativeElement;
const badge1El = badgeDes1[0].nativeElement;
- // key debug elements
- const keyDes0 = getAndExpectDebugElementByCss(
+ expect(badge0El.textContent).toBeDefined();
+ expect(badge0El.textContent).toContain(
+ expectedIncoming[0].links.length,
+ `should contain ${expectedIncoming[0].links.length}`
+ );
+ expect(badge1El.textContent).toBeDefined();
+ expect(badge1El.textContent).toContain(
+ expectedIncoming[1].links.length,
+ `should contain ${expectedIncoming[1].links.length}`
+ );
+ });
+
+ it('... should render restype label in panel header (div.card-header)', () => {
+ // header debug element
+ const panelHeaderDes = getAndExpectDebugElementByCss(compDe, 'div.card > div.card-header', 2, 2);
+ const labelDes0 = getAndExpectDebugElementByCss(
panelHeaderDes[0],
'span.awg-linked-obj-title',
1,
1,
'in first panel'
);
- const keyDes1 = getAndExpectDebugElementByCss(
+ const labelDes1 = getAndExpectDebugElementByCss(
panelHeaderDes[1],
'span.awg-linked-obj-title',
1,
@@ -246,36 +264,18 @@ describe('ResourceDetailHtmlContentLinkedobjectsComponent (DONE)', () => {
'in second panel'
);
- // key native elements
- const key0El = keyDes0[0].nativeElement;
- const key1El = keyDes1[0].nativeElement;
-
- // create pipe
- keyValuePipe = new KeyValuePipe(defaultKeyValueDiffers);
- const pipedExpectedIncoming = keyValuePipe.transform(expectedIncoming);
-
- // check badge output
- expect(badge0El.textContent).toBeDefined();
- expect(badge0El.textContent).toContain(
- pipedExpectedIncoming[0].value.length.toString(),
- `should contain ${pipedExpectedIncoming[0].value.length}`
- );
- expect(badge1El.textContent).toBeDefined();
- expect(badge1El.textContent).toContain(
- pipedExpectedIncoming[1].value.length.toString(),
- `should contain ${pipedExpectedIncoming[1].value.length}`
- );
+ const label0El = labelDes0[0].nativeElement;
+ const label1El = labelDes1[0].nativeElement;
- // check key output
- expect(key0El.textContent).toBeDefined();
- expect(key0El.textContent).toContain(
- pipedExpectedIncoming[0].key,
- `should contain ${pipedExpectedIncoming[0].key}`
+ expect(label0El.textContent).toBeDefined();
+ expect(label0El.textContent).toContain(
+ expectedIncoming[0].restypeLabel,
+ `should contain ${expectedIncoming[0].restypeLabel}`
);
- expect(key1El.textContent).toBeDefined();
- expect(key1El.textContent).toContain(
- pipedExpectedIncoming[1].key,
- `should contain ${pipedExpectedIncoming[1].key}`
+ expect(label0El.textContent).toBeDefined();
+ expect(label1El.textContent).toContain(
+ expectedIncoming[1].restypeLabel,
+ `should contain ${expectedIncoming[1].restypeLabel}`
);
});
@@ -350,143 +350,101 @@ describe('ResourceDetailHtmlContentLinkedobjectsComponent (DONE)', () => {
expectOpenPanelBody(compDe, 1, 'opened (second panel)');
});
- it('... should render incomingLinks in table of panel content (div.card-body)', () => {
- const id0 = expectedIncoming['testkey1'][0].id;
- const id1 = expectedIncoming['testkey1'][1].id;
- const value0 = expectedIncoming['testkey1'][0].value;
- const value1 = expectedIncoming['testkey1'][1].value;
-
- // button debug elements
- const buttonDes = getAndExpectDebugElementByCss(
- compDe,
- 'div.card > div.card-header button.btn-link',
- 2,
- 2
- );
-
- // first button's native element to click on
- const button0El = buttonDes[0].nativeElement;
-
- // open first panel
- click(button0El as HTMLElement);
- fixture.detectChanges();
-
- expectOpenPanelBody(compDe, 0, 'should have first panel opened');
-
- // table debug elements
- const tableDes = getAndExpectDebugElementByCss(compDe, 'table.awg-linked-obj-table', 1, 1);
-
- // img
- const imgDes = getAndExpectDebugElementByCss(tableDes[0], 'a.awg-linked-obj-link > img', 2, 2);
- const icon0 = expectedIncoming['testkey1'][0].restype.icon;
- const icon1 = expectedIncoming['testkey1'][1].restype.icon;
-
- // spanId
- const spanIdDes = getAndExpectDebugElementByCss(
- tableDes[0],
- 'a.awg-linked-obj-link > span.awg-linked-obj-link-id',
- 2,
- 2
- );
-
- // spanValue
- const spanValueDes = getAndExpectDebugElementByCss(
- tableDes[0],
- 'a.awg-linked-obj-link > span.awg-linked-obj-link-value',
- 2,
- 2
- );
-
- // native elements
- const imgEl0 = imgDes[0].nativeElement;
- const imgEl1 = imgDes[1].nativeElement;
-
- const spanIdEl0 = spanIdDes[0].nativeElement;
- const spanIdEl1 = spanIdDes[1].nativeElement;
-
- const spanValueEl0 = spanValueDes[0].nativeElement;
- const spanValueEl1 = spanValueDes[1].nativeElement;
-
- // check img output
- expect(imgEl0.src).toBeDefined();
- expect(imgEl0.src).toContain(icon0, `should contain icon0: ${icon0}`);
-
- expect(imgEl1.src).toBeDefined();
- expect(imgEl1.src).toContain(icon1, `should contain icon1: ${icon1}`);
-
- // check id output
- expect(spanIdEl0.textContent).toBeDefined();
- expect(spanIdEl0.textContent).toBe(id0, `should be id0: ${id0}`);
-
- expect(spanIdEl1.textContent).toBeDefined();
- expect(spanIdEl1.textContent).toContain(id1, `should contain id1: ${id1}`);
-
- // check value output
- expect(spanValueEl0.textContent).toBeDefined();
- expect(spanValueEl0.textContent).toContain(value0, `should contain value0: ${value0}`);
-
- expect(spanValueEl1.textContent).toBeDefined();
- expect(spanValueEl1.textContent).toContain(value1, `should contain value1: ${value1}`);
+ describe('... should render panel content (div.card-body)', () => {
+ let tableDes: DebugElement[];
+
+ beforeEach(() => {
+ /**
+ * Click button to open first panel and get inner table
+ */
+
+ // button debug elements
+ const buttonDes = getAndExpectDebugElementByCss(
+ compDe,
+ 'div.card > div.card-header button.btn-link',
+ 2,
+ 2
+ );
+
+ // first button's native element to click on
+ const button0El = buttonDes[0].nativeElement;
+
+ // open first panel
+ click(button0El as HTMLElement);
+ fixture.detectChanges();
+
+ expectOpenPanelBody(compDe, 0, 'should have first panel opened');
+
+ // table debug elements
+ tableDes = getAndExpectDebugElementByCss(compDe, 'table.awg-linked-obj-table', 1, 1);
+ });
+
+ it('... should render restype icon', () => {
+ const expectedIcon0 = expectedIncoming[0].links[0].restype.icon;
+ const expectedIcon1 = expectedIncoming[0].links[1].restype.icon;
+
+ const imgDes = getAndExpectDebugElementByCss(tableDes[0], 'a.awg-linked-obj-link > img', 2, 2);
+
+ const imgEl0 = imgDes[0].nativeElement;
+ const imgEl1 = imgDes[1].nativeElement;
+
+ expect(imgEl0.src).toBeDefined();
+ expect(imgEl0.src).toContain(expectedIcon0, `should contain expectedIcon0: ${expectedIcon0}`);
+
+ expect(imgEl1.src).toBeDefined();
+ expect(imgEl1.src).toContain(expectedIcon1, `should contain expectedIcon1: ${expectedIcon1}`);
+ });
+
+ it('... should render restpye id', () => {
+ const expectedId0 = expectedIncoming[0].links[0].id;
+ const expectedId1 = expectedIncoming[0].links[1].id;
+
+ const idDes = getAndExpectDebugElementByCss(
+ tableDes[0],
+ 'a.awg-linked-obj-link > span.awg-linked-obj-link-id',
+ 2,
+ 2
+ );
+
+ const idEl0 = idDes[0].nativeElement;
+ const idEl1 = idDes[1].nativeElement;
+
+ expect(idEl0.textContent).toBeDefined();
+ expect(idEl0.textContent).toBe(expectedId0, `should be expectedId0: ${expectedId0}`);
+
+ expect(idEl1.textContent).toBeDefined();
+ expect(idEl1.textContent).toContain(expectedId1, `should contain expectedId1: ${expectedId1}`);
+ });
+
+ it('... should render link value', () => {
+ const expectedLinkValue0 = expectedIncoming[0].links[0].value;
+ const expectedLinkValue1 = expectedIncoming[0].links[1].value;
+
+ const linkValueDes = getAndExpectDebugElementByCss(
+ tableDes[0],
+ 'a.awg-linked-obj-link > span.awg-linked-obj-link-value',
+ 2,
+ 2
+ );
+
+ const linkValueEl0 = linkValueDes[0].nativeElement;
+ const linkValueEl1 = linkValueDes[1].nativeElement;
+
+ expect(linkValueEl0.textContent).toBeDefined();
+ expect(linkValueEl0.textContent).toContain(
+ expectedLinkValue0,
+ `should contain expectedLinkValue0: ${expectedLinkValue0}`
+ );
+
+ expect(linkValueEl1.textContent).toBeDefined();
+ expect(linkValueEl1.textContent).toContain(
+ expectedLinkValue1,
+ `should contain expectedLinkValue1: ${expectedLinkValue1}`
+ );
+ });
});
});
- describe('#updateTotalNumber', () => {
- it('... should have been called', fakeAsync(() => {
- expectSpyCall(updateTotalNumberSpy, 1);
- }));
-
- it('... should have updated totalNumber with number of nested array items', fakeAsync(() => {
- expectSpyCall(updateTotalNumberSpy, 1);
-
- expect(component.totalNumber).toBe(
- expectedTotalItems,
- `should be expectedTotalItems: ${expectedTotalItems}`
- );
- }));
-
- it('... should do nothing on first onChanges', fakeAsync(() => {
- const newExpectedIncoming: ResourceDetailGroupedIncomingLinks = {
- testkey1: [incomingLink1, incomingLink2]
- };
-
- // simulate the parent changing the input properties for the first time
- component.incoming = newExpectedIncoming;
-
- // trigger ngOnChanges
- component.ngOnChanges({ incoming: new SimpleChange(null, component.incoming, true) });
- fixture.detectChanges();
-
- // spy has been called only once with ngOnInit
- expectSpyCall(updateTotalNumberSpy, 1);
-
- // output has not changed
- expect(component.totalNumber).toBe(
- expectedTotalItems,
- `should be still expectedTotalItems: ${expectedTotalItems}`
- );
- }));
-
- it('... should recalculate total number on input changes (second & more)', fakeAsync(() => {
- const newExpectedIncoming: ResourceDetailGroupedIncomingLinks = {
- testkey1: [incomingLink1, incomingLink2, incomingLink3]
- };
- const newTotalItems = newExpectedIncoming['testkey1'].length;
-
- // simulate the parent changing the input properties for the first time
- component.incoming = newExpectedIncoming;
-
- // trigger ngOnChanges
- component.ngOnChanges({ incoming: new SimpleChange(null, component.incoming, false) });
- fixture.detectChanges();
-
- // spy has been called twice now
- expectSpyCall(updateTotalNumberSpy, 2);
-
- // output has changed
- expect(component.totalNumber).toBe(newTotalItems, `should be newTotalItems: ${newTotalItems}`);
- }));
- });
-
describe('#navigateToResource', () => {
beforeEach(fakeAsync(() => {
// open second panel
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/linkedobjects/linkedobjects.component.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/linkedobjects/linkedobjects.component.ts
index f8f8ca1d3a..d2ff11bfcf 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/linkedobjects/linkedobjects.component.ts
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/linkedobjects/linkedobjects.component.ts
@@ -1,33 +1,53 @@
-import { Component, EventEmitter, Input, OnInit, OnChanges, Output, SimpleChanges } from '@angular/core';
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
-import { ConversionService } from '@awg-core/services/conversion-service';
import { ResourceDetailGroupedIncomingLinks } from '@awg-views/data-view/models';
+/**
+ * The ResourceDetailHtmlContentLinkedobjects component.
+ *
+ * It displays the incoming links of a resource detail and
+ * their total number.
+ */
@Component({
selector: 'awg-resource-detail-html-content-linkedobjects',
templateUrl: './linkedobjects.component.html',
- styleUrls: ['./linkedobjects.component.css']
+ styleUrls: ['./linkedobjects.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
-export class ResourceDetailHtmlContentLinkedobjectsComponent implements OnInit, OnChanges {
+export class ResourceDetailHtmlContentLinkedobjectsComponent {
+ /**
+ * Input variable: incomingGroups.
+ *
+ * It keeps the grouped incoming links array.
+ */
@Input()
- incoming: ResourceDetailGroupedIncomingLinks;
+ incomingGroups: ResourceDetailGroupedIncomingLinks[];
+
+ /**
+ * Output variable: resourceRequest.
+ *
+ * It keeps an event emitter for the resource request.
+ */
@Output()
resourceRequest: EventEmitter = new EventEmitter();
- totalNumber = 0;
-
- constructor(private conversionService: ConversionService) {}
-
- ngOnInit() {
- this.updateTotalNumber();
- }
-
- ngOnChanges(changes: SimpleChanges) {
- if (!changes['incoming'].isFirstChange()) {
- this.updateTotalNumber();
- }
+ /**
+ * Getter for the total number of incoming links.
+ */
+ get totalNumber() {
+ return this.getNestedArraysTotalItems(this.incomingGroups) || 0;
}
+ /**
+ * Public method: navigateToResource.
+ *
+ * It emits the given id to be handed
+ * to the parent component.
+ *
+ * @param {string} id The given resource id.
+ *
+ * @returns {void} Emits the id.
+ */
navigateToResource(id?: string): void {
if (!id) {
return;
@@ -36,12 +56,27 @@ export class ResourceDetailHtmlContentLinkedobjectsComponent implements OnInit,
this.resourceRequest.emit(id);
}
- updateTotalNumber() {
- this.totalNumber = this.getNestedArraysTotalItems(this.incoming);
- }
+ /**
+ * Private method: getNestedArraysTotalItems.
+ *
+ * It sums up the total items (length) of all arrays
+ * nested in an ResourceDetailGroupedIncomingLinks
+ * array.
+ *
+ * @param {ResourceDetailGroupedIncomingLinks[]} groupedLinksArr The given grouped incoming links array.
+ *
+ * @returns {number} The number of total items (length) of the nested array.
+ */
+ private getNestedArraysTotalItems(groupedLinksArr: ResourceDetailGroupedIncomingLinks[]): number {
+ if (!groupedLinksArr) {
+ return;
+ }
+
+ // callback for reduce function
+ // adds curValue to the previous result of the calculation (prevRes))
+ const reducer = (prevRes: number, curValue: number): number => prevRes + curValue;
- private getNestedArraysTotalItems(obj: ResourceDetailGroupedIncomingLinks): number {
- // sum up length of all arrays nested in linked objects object
- return this.conversionService.getNestedArraysTotalItems(obj);
+ // map the length of every nested link array into the reducer function; default initial value: 0
+ return groupedLinksArr.map(groupedLink => groupedLink.links.length).reduce(reducer, 0);
}
}
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.html b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.html
index ad94db404e..2e7d081ced 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.html
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.html
@@ -3,15 +3,15 @@
Objektdaten
-
+
-
+
{{ prop?.label }}
-
+
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.spec.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.spec.ts
index adeb8264e9..fa7dd70023 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.spec.ts
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.spec.ts
@@ -10,7 +10,7 @@ import {
} from '@testing/expect-helper';
import { CompileHtmlComponent } from '@awg-shared/compile-html';
-import { ResourceDetailProps } from '@awg-views/data-view/models';
+import { ResourceDetailProperty } from '@awg-views/data-view/models';
import { ResourceDetailHtmlContentPropsComponent } from './props.component';
@@ -23,7 +23,7 @@ describe('ResourceDetailHtmlContentPropsComponent (DONE)', () => {
let navigateToResourceSpy: Spy;
let emitSpy: Spy;
- let expectedProps: ResourceDetailProps[];
+ let expectedProps: ResourceDetailProperty[];
let expectedMetaBreakLine: string;
beforeEach(async(() => {
@@ -42,30 +42,17 @@ describe('ResourceDetailHtmlContentPropsComponent (DONE)', () => {
expectedMetaBreakLine = 'Versionsdatum';
const prop1Value1 = `Op. 28 : Skizzen zu einem "1. Satz" (später 2. Satz [ M 330 ])`;
- const props1: ResourceDetailProps = {
- pid: '0',
- guielement: 'text',
- label: 'prop1',
- value: [prop1Value1, 'prop1-value2']
- };
- const props2: ResourceDetailProps = {
- pid: '1',
- guielement: 'date',
- label: 'Versionsdatum',
- value: ['2019']
- };
- const props3: ResourceDetailProps = {
- pid: '2',
- guielement: 'richtext',
- label: 'prop2',
- value: ['prop2-value1', 'prop2-value2', 'prop2-value3']
- };
- const props4: ResourceDetailProps = {
- pid: '3',
- guielement: 'text',
- label: 'prop1',
- value: []
- };
+ const props1: ResourceDetailProperty = new ResourceDetailProperty('0', 'text', 'prop1', [
+ prop1Value1,
+ 'prop1-value2'
+ ]);
+ const props2: ResourceDetailProperty = new ResourceDetailProperty('1', 'date', 'Versionsdatum', ['2019']);
+ const props3: ResourceDetailProperty = new ResourceDetailProperty('2', 'richtext', 'prop2', [
+ 'prop2-value1',
+ 'prop2-value2',
+ 'prop2-value3'
+ ]);
+ const props4: ResourceDetailProperty = new ResourceDetailProperty('3', 'text', 'prop1', []);
expectedProps = [props1, props2, props3, props4];
// spies on component functions
@@ -148,9 +135,9 @@ describe('ResourceDetailHtmlContentPropsComponent (DONE)', () => {
it('... should contain inner ul elements according to each props.value.length', () => {
const outerLiDes = getAndExpectDebugElementByCss(compDe, 'ul > li.awg-prop', 3, 3);
- const expectedLength0 = expectedProps[0].value.length;
- const expectedLength1 = expectedProps[1].value.length;
- const expectedLength2 = expectedProps[2].value.length;
+ const expectedLength0 = expectedProps[0].values.length;
+ const expectedLength1 = expectedProps[1].values.length;
+ const expectedLength2 = expectedProps[2].values.length;
getAndExpectDebugElementByCss(outerLiDes[0], 'ul', expectedLength0, expectedLength0);
getAndExpectDebugElementByCss(outerLiDes[1], 'ul', expectedLength1, expectedLength1);
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.ts
index b6c506ff15..2f1441e016 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.ts
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/props/props.component.ts
@@ -1,24 +1,62 @@
-import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
-import { ResourceDetailProps } from '@awg-views/data-view/models';
+import { ResourceDetailProperty } from '@awg-views/data-view/models';
+/**
+ * The ResourceDetailHtmlContentProps component.
+ *
+ * It displays the properties of a resource detail
+ * of the data view of the app.
+ */
@Component({
selector: 'awg-resource-detail-html-content-props',
templateUrl: './props.component.html',
- styleUrls: ['./props.component.css']
+ styleUrls: ['./props.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class ResourceDetailHtmlContentPropsComponent implements OnInit {
+ /**
+ * Input variable: props.
+ *
+ * It keeps the properties for the resource detail.
+ */
@Input()
- props: ResourceDetailProps[];
+ props: ResourceDetailProperty[];
+
+ /**
+ * Output variable: resourceRequest.
+ *
+ * It keeps an event emitter for the selected id of a resource.
+ */
@Output()
resourceRequest: EventEmitter = new EventEmitter();
+ /**
+ * Public variable: metaBreakLine.
+ *
+ * It keeps the property key for a breakline
+ * between the resource detail properties.
+ */
metaBreakLine = 'Versionsdatum';
- constructor() {}
-
+ /**
+ * Angular life cycle hook: ngOnInit.
+ *
+ * It calls the containing methods
+ * when initializing the component.
+ */
ngOnInit() {}
+ /**
+ * Public method: navigateToResource.
+ *
+ * It emits a given id of a selected resource
+ * to the {@link resourceRequest}.
+ *
+ * @param {string} id The given resource id.
+ *
+ * @returns {void} Emits the id.
+ */
navigateToResource(id?: string): void {
if (!id) {
return;
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.html b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.html
index 477e177381..d3fd0a9222 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.html
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.html
@@ -15,31 +15,28 @@
-
-
-
0"
- [images]="content?.images">
+
-
-
+
-
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.spec.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.spec.ts
index 57c46c29c9..6459afbb72 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.spec.ts
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.spec.ts
@@ -3,6 +3,7 @@ import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/c
import Spy = jasmine.Spy;
import { JsonConvert } from 'json2typescript';
+import { NgxGalleryImage } from 'ngx-gallery';
import {
expectSpyCall,
@@ -16,7 +17,7 @@ import {
ResourceDetailContent,
ResourceDetailGroupedIncomingLinks,
ResourceDetailImage,
- ResourceDetailProps
+ ResourceDetailProperty
} from '@awg-views/data-view/models';
import { ResourceDetailHtmlContentComponent } from './resource-detail-html-content.component';
@@ -25,7 +26,7 @@ import { ResourceDetailHtmlContentComponent } from './resource-detail-html-conte
@Component({ selector: 'awg-resource-detail-html-content-props', template: '' })
class ResourceDetailHtmlContentPropsStubComponent {
@Input()
- props: ResourceDetailProps[];
+ props: ResourceDetailProperty[];
@Output()
resourceRequest: EventEmitter
= new EventEmitter();
}
@@ -33,13 +34,13 @@ class ResourceDetailHtmlContentPropsStubComponent {
@Component({ selector: 'awg-resource-detail-html-content-imageobjects', template: '' })
class ResourceDetailHtmlContentImageobjectsStubComponent {
@Input()
- images: ResourceDetailImage[];
+ images: NgxGalleryImage[];
}
@Component({ selector: 'awg-resource-detail-html-content-linkedobjects', template: '' })
class ResourceDetailHtmlContentLinkedobjectsStubComponent {
@Input()
- incoming: ResourceDetailGroupedIncomingLinks;
+ incomingGroups: ResourceDetailGroupedIncomingLinks[];
@Output()
resourceRequest: EventEmitter = new EventEmitter();
}
@@ -85,11 +86,11 @@ describe('ResourceDetailHtmlContentComponent (DONE)', () => {
new ResourceDetailImage(context, 0),
new ResourceDetailImage(context, 1)
];
- const incoming = new ResourceDetailGroupedIncomingLinks();
- const props: ResourceDetailProps[] = [
- { pid: '1', guielement: 'text', label: 'Test-Label', value: ['Test1', 'Test2'] }
+ const incoming = [new ResourceDetailGroupedIncomingLinks()];
+ const props: ResourceDetailProperty[] = [
+ new ResourceDetailProperty('1', 'text', 'Test-Label', ['Test1', 'Test2'])
];
- expectedContent = { props, images, incoming };
+ expectedContent = new ResourceDetailContent(props, images, incoming);
// spies on component functions
// `.and.callThrough` will track the spy down the nested describes, see
@@ -129,16 +130,16 @@ describe('ResourceDetailHtmlContentComponent (DONE)', () => {
getAndExpectDebugElementByCss(compDe, 'div.col-lg-4 > div.sidenav-right', 1, 1);
});
- it('... should not contain ResourceDetailHtmlContentPropsComponent (stubbed)', () => {
- getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentPropsStubComponent, 0, 0);
+ it('... should contain one ResourceDetailHtmlContentPropsComponent (stubbed)', () => {
+ getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentPropsStubComponent, 1, 1);
});
- it('... should not contain ResourceDetailHtmlContentImageobjectsComponent (stubbed)', () => {
- getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentImageobjectsStubComponent, 0, 0);
+ it('... should contain one ResourceDetailHtmlContentImageobjectsComponent (stubbed)', () => {
+ getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentImageobjectsStubComponent, 1, 1);
});
- it('... should not contain ResourceDetailHtmlContentLinkedobjectsComponent (stubbed)', () => {
- getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentLinkedobjectsStubComponent, 0, 0);
+ it('... should contain one ResourceDetailHtmlContentLinkedobjectsComponent (stubbed)', () => {
+ getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentLinkedobjectsStubComponent, 1, 1);
});
});
});
@@ -158,51 +159,6 @@ describe('ResourceDetailHtmlContentComponent (DONE)', () => {
});
describe('VIEW', () => {
- it('... should contain one ResourceDetailHtmlContentPropsComponent (stubbed)', () => {
- getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentPropsStubComponent, 1, 1);
- });
-
- it('... should contain one ResourceDetailHtmlContentImageobjectsComponent (stubbed)', () => {
- getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentImageobjectsStubComponent, 1, 1);
- });
-
- it('... should contain one ResourceDetailHtmlContentLinkedobjectsComponent (stubbed)', () => {
- getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentLinkedobjectsStubComponent, 1, 1);
- });
-
- it('... should contain one ResourceDetailHtmlContentPropsComponent (stubbed) only if props provided', () => {
- getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentPropsStubComponent, 1, 1);
-
- // provide data without props property
- expectedContent.props = null;
- component.content = expectedContent;
- fixture.detectChanges();
-
- getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentPropsStubComponent, 0, 0);
- });
-
- it('... should contain one ResourceDetailHtmlContentImageobjectsComponent (stubbed) only if images provided', () => {
- getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentImageobjectsStubComponent, 1, 1);
-
- // provide data without images property
- expectedContent.images = [];
- component.content = expectedContent;
- fixture.detectChanges();
-
- getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentImageobjectsStubComponent, 0, 0);
- });
-
- it('... should contain one ResourceDetailHtmlContentLinkedobjectsComponent (stubbed) only if incoming provided', () => {
- getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentLinkedobjectsStubComponent, 1, 1);
-
- // provide data without incoming property
- expectedContent.incoming = null;
- component.content = expectedContent;
- fixture.detectChanges();
-
- getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentLinkedobjectsStubComponent, 0, 0);
- });
-
it('... should pass down `content.props` to ResourceDetailHtmlContentPropsComponent', () => {
// get debug and native element of stubbed child
const propsDes = getAndExpectDebugElementByDirective(
@@ -247,8 +203,8 @@ describe('ResourceDetailHtmlContentComponent (DONE)', () => {
ResourceDetailHtmlContentLinkedobjectsStubComponent
) as ResourceDetailHtmlContentLinkedobjectsStubComponent;
- expect(incomingCmp.incoming).toBeDefined();
- expect(incomingCmp.incoming).toBe(expectedContent.incoming);
+ expect(incomingCmp.incomingGroups).toBeDefined();
+ expect(incomingCmp.incomingGroups).toBe(expectedContent.incoming);
});
});
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.ts
index 568283bb28..aef9274aa7 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.ts
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html-content/resource-detail-html-content.component.ts
@@ -1,22 +1,57 @@
-import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ResourceDetailContent } from '@awg-views/data-view/models';
+/**
+ * The ResourceDetailHtmlContent component.
+ *
+ * It contains the content section of a resource detail
+ * of the data view of the app
+ * with a {@link ResourceDetailHtmlContentPropsComponent},
+ * a {@link ResourceDetailHtmlContentImageobjectsComponent}
+ * and the {@link ResourceDetailHtmlContentLinkedobjectsComponent}.
+ */
@Component({
selector: 'awg-resource-detail-html-content',
templateUrl: './resource-detail-html-content.component.html',
- styleUrls: ['./resource-detail-html-content.component.css']
+ styleUrls: ['./resource-detail-html-content.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class ResourceDetailHtmlContentComponent implements OnInit {
+ /**
+ * Input variable: content.
+ *
+ * It keeps the html content for the resource detail.
+ */
@Input()
content: ResourceDetailContent;
+
+ /**
+ * Output variable: resourceRequest.
+ *
+ * It keeps an event emitter for the selected id of a resource.
+ */
@Output()
resourceRequest: EventEmitter = new EventEmitter();
- constructor() {}
-
+ /**
+ * Angular life cycle hook: ngOnInit.
+ *
+ * It calls the containing methods
+ * when initializing the component.
+ */
ngOnInit() {}
+ /**
+ * Public method: navigateToResource.
+ *
+ * It emits a given id of a selected resource
+ * to the {@link resourceRequest}.
+ *
+ * @param {string} id The given resource id.
+ *
+ * @returns {void} Emits the id.
+ */
navigateToResource(id?: string): void {
if (!id) {
return;
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.html b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.html
index 71649a85a7..e5956ea95f 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.html
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.html
@@ -1,8 +1,7 @@
-
+ (resourceRequest)="navigateToResource($event)"
+>
-
-
-
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.spec.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.spec.ts
index 6639755ce6..eba549fd2c 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.spec.ts
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.spec.ts
@@ -11,7 +11,7 @@ import {
ResourceDetailGroupedIncomingLinks,
ResourceDetailHeader,
ResourceDetailImage,
- ResourceDetailProps
+ ResourceDetailProperty
} from '@awg-views/data-view/models';
import { ResourceDetailHtmlComponent } from './resource-detail-html.component';
@@ -51,9 +51,9 @@ describe('ResourceDetailHtmlComponent (DONE)', () => {
// test data
const header: ResourceDetailHeader = { objID: '1234', icon: '', type: '', title: 'Test', lastmod: '' };
const images: ResourceDetailImage[] = [];
- const incoming = new ResourceDetailGroupedIncomingLinks();
- const props: ResourceDetailProps[] = [
- { pid: '1', guielement: 'text', label: 'Test-Label', value: ['Test1', 'Test2'] }
+ const incoming = [new ResourceDetailGroupedIncomingLinks()];
+ const props: ResourceDetailProperty[] = [
+ new ResourceDetailProperty('1', 'text', 'Test-Label', ['Test1', 'Test2'])
];
const content: ResourceDetailContent = { props, images, incoming };
@@ -90,21 +90,12 @@ describe('ResourceDetailHtmlComponent (DONE)', () => {
});
describe('AFTER initial data binding', () => {
- let htmlContentDes: DebugElement[];
- let htmlContentCmp: any;
-
beforeEach(() => {
// simulate the parent setting the input properties
component.resourceDetailData = expectedResourceDetailData;
// trigger initial data binding
fixture.detectChanges();
-
- // get debug and native element of stubbed child
- htmlContentDes = getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentStubComponent, 1, 1);
- htmlContentCmp = htmlContentDes[0].injector.get(
- ResourceDetailHtmlContentStubComponent
- ) as ResourceDetailHtmlContentStubComponent;
});
it('should have `resourceDetailData` inputs', () => {
@@ -114,7 +105,7 @@ describe('ResourceDetailHtmlComponent (DONE)', () => {
describe('VIEW', () => {
it('should contain resource detail html content component (stubbed)', () => {
- htmlContentDes = getAndExpectDebugElementByDirective(
+ const htmlContentDes = getAndExpectDebugElementByDirective(
compDe,
ResourceDetailHtmlContentStubComponent,
1,
@@ -123,7 +114,7 @@ describe('ResourceDetailHtmlComponent (DONE)', () => {
});
it('should contain resource detail html content component (stubbed) only if content provided', () => {
- htmlContentDes = getAndExpectDebugElementByDirective(
+ const firstContentDes = getAndExpectDebugElementByDirective(
compDe,
ResourceDetailHtmlContentStubComponent,
1,
@@ -131,14 +122,38 @@ describe('ResourceDetailHtmlComponent (DONE)', () => {
);
// provide data without content property
- expectedResourceDetailData.content = null;
+ expectedResourceDetailData = new ResourceDetail(undefined, undefined);
+
+ // simulate the host parent setting the new input properties
component.resourceDetailData = expectedResourceDetailData;
fixture.detectChanges();
- getAndExpectDebugElementByDirective(compDe, ResourceDetailHtmlContentStubComponent, 0, 0);
+ // TODO: should be 0
+ // check angular defect with testing under ChangeDetectionStrategy.OnPush : https://github.com/angular/angular/issues/12313
+
+ const newContentDes = getAndExpectDebugElementByDirective(
+ compDe,
+ ResourceDetailHtmlContentStubComponent,
+ 1,
+ 1
+ );
+ const newContentEl = newContentDes[0].nativeElement;
+
+ expect(newContentEl.innerHTML).toBeDefined();
+ expect(newContentEl.innerHTML).toBe('', 'should be empty string');
});
it('... should pass down `resourceDetailData` to resource detail html content component', () => {
+ const htmlContentDes = getAndExpectDebugElementByDirective(
+ compDe,
+ ResourceDetailHtmlContentStubComponent,
+ 1,
+ 1
+ );
+ const htmlContentCmp = htmlContentDes[0].injector.get(
+ ResourceDetailHtmlContentStubComponent
+ ) as ResourceDetailHtmlContentStubComponent;
+
expect(htmlContentCmp.content).toBeDefined();
expect(htmlContentCmp.content).toBe(expectedResourceDetailData.content);
});
@@ -146,6 +161,16 @@ describe('ResourceDetailHtmlComponent (DONE)', () => {
describe('#navigateToResource', () => {
it('... should trigger on event from ResourceDetailHtmlContentComponent', fakeAsync(() => {
+ const htmlContentDes = getAndExpectDebugElementByDirective(
+ compDe,
+ ResourceDetailHtmlContentStubComponent,
+ 1,
+ 1
+ );
+ const htmlContentCmp = htmlContentDes[0].injector.get(
+ ResourceDetailHtmlContentStubComponent
+ ) as ResourceDetailHtmlContentStubComponent;
+
let id;
// undefined
@@ -168,6 +193,16 @@ describe('ResourceDetailHtmlComponent (DONE)', () => {
}));
it('... should not emit anything if no id is provided', fakeAsync(() => {
+ const htmlContentDes = getAndExpectDebugElementByDirective(
+ compDe,
+ ResourceDetailHtmlContentStubComponent,
+ 1,
+ 1
+ );
+ const htmlContentCmp = htmlContentDes[0].injector.get(
+ ResourceDetailHtmlContentStubComponent
+ ) as ResourceDetailHtmlContentStubComponent;
+
htmlContentCmp.resourceRequest.emit(undefined);
// id is undefined
@@ -176,6 +211,16 @@ describe('ResourceDetailHtmlComponent (DONE)', () => {
}));
it('... should emit provided resource id (as string) on click', fakeAsync(() => {
+ const htmlContentDes = getAndExpectDebugElementByDirective(
+ compDe,
+ ResourceDetailHtmlContentStubComponent,
+ 1,
+ 1
+ );
+ const htmlContentCmp = htmlContentDes[0].injector.get(
+ ResourceDetailHtmlContentStubComponent
+ ) as ResourceDetailHtmlContentStubComponent;
+
let id;
// number
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.ts
index 5275216701..fc9ea6dbaf 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.ts
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-html/resource-detail-html.component.ts
@@ -1,23 +1,55 @@
-import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ResourceDetail } from '@awg-views/data-view/models';
+/**
+ * The ResourceDetailHtml component.
+ *
+ * It contains the html representation
+ * of a resource detail of the data view of the app
+ * with a {@link ResourceDetailHtmlContentComponent}.
+ */
@Component({
selector: 'awg-resource-detail-html',
templateUrl: './resource-detail-html.component.html',
- styleUrls: ['./resource-detail-html.component.css']
+ styleUrls: ['./resource-detail-html.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class ResourceDetailHtmlComponent implements OnInit {
+ /**
+ * Input variable: resourceDetailData.
+ *
+ * It keeps the header for the resource detail.
+ */
@Input()
resourceDetailData: ResourceDetail;
+
+ /**
+ * Output variable: resourceRequest.
+ *
+ * It keeps an event emitter for the selected id of a resource.
+ */
@Output()
resourceRequest: EventEmitter = new EventEmitter();
- constructor() {}
-
+ /**
+ * Angular life cycle hook: ngOnInit.
+ *
+ * It calls the containing methods
+ * when initializing the component.
+ */
ngOnInit() {}
- navigateToResource(id?: string) {
+ /**
+ * Public method: navigateToResource.
+ *
+ * It emits a given id of a selected resource
+ * to the {@link resourceRequest}.
+ *
+ * @param {string} id The given resource id.
+ * @returns {void} Emits the id.
+ */
+ navigateToResource(id?: string): void {
if (!id) {
return;
}
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-converted/resource-detail-json-converted.component.html b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-converted/resource-detail-json-converted.component.html
index e4eef4e498..f4654cb809 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-converted/resource-detail-json-converted.component.html
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-converted/resource-detail-json-converted.component.html
@@ -1,4 +1,5 @@
+ jsonViewerHeader="Converted JSON response from Salsah-API"
+ [jsonViewerData]="resourceJsonConvertedData"
+>
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-converted/resource-detail-json-converted.component.spec.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-converted/resource-detail-json-converted.component.spec.ts
index d43bb3eabf..680e658b65 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-converted/resource-detail-json-converted.component.spec.ts
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-converted/resource-detail-json-converted.component.spec.ts
@@ -41,7 +41,7 @@ describe('ResourceDetailJsonConvertedComponent (DONE)', () => {
// test data
expectedData = new ResourceDetail(
{ objID: '', icon: '', type: '', title: 'test', lastmod: '2019' },
- new ResourceDetailContent()
+ new ResourceDetailContent(undefined, undefined, undefined)
);
expectedData.header = { objID: '', icon: '', type: '', title: 'test', lastmod: '2019' };
});
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-converted/resource-detail-json-converted.component.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-converted/resource-detail-json-converted.component.ts
index f6e0a1100e..6398441ea5 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-converted/resource-detail-json-converted.component.ts
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-converted/resource-detail-json-converted.component.ts
@@ -1,17 +1,34 @@
-import { Component, Input, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { ResourceDetail } from '@awg-views/data-view/models';
+/**
+ * The ResourceDetailJsonConverted component.
+ *
+ * It contains the converted json representation
+ * of a resource detail of the data view of the app
+ * with a {@link JsonViewerComponent}.
+ */
@Component({
selector: 'awg-resource-detail-json-converted',
templateUrl: './resource-detail-json-converted.component.html',
- styleUrls: ['./resource-detail-json-converted.component.css']
+ styleUrls: ['./resource-detail-json-converted.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class ResourceDetailJsonConvertedComponent implements OnInit {
+ /**
+ * Input variable: resourceJsonConvertedData.
+ *
+ * It keeps the converted json data for the resource detail.
+ */
@Input()
resourceJsonConvertedData: ResourceDetail;
- constructor() {}
-
+ /**
+ * Angular life cycle hook: ngOnInit.
+ *
+ * It calls the containing methods
+ * when initializing the component.
+ */
ngOnInit() {}
}
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-raw/resource-detail-json-raw.component.html b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-raw/resource-detail-json-raw.component.html
index 083ead3c19..228495f18b 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-raw/resource-detail-json-raw.component.html
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-raw/resource-detail-json-raw.component.html
@@ -1,5 +1,2 @@
-
+
-
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-raw/resource-detail-json-raw.component.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-raw/resource-detail-json-raw.component.ts
index 47b5e880c9..030b5bdfdf 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-raw/resource-detail-json-raw.component.ts
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-json-raw/resource-detail-json-raw.component.ts
@@ -1,17 +1,34 @@
-import { Component, Input, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { ResourceFullResponseJson } from '@awg-shared/api-objects';
+/**
+ * The ResourceDetailJsonRaw component.
+ *
+ * It contains the raw json representation
+ * of a resource detail of the data view of the app
+ * with a {@link JsonViewerComponent}.
+ */
@Component({
selector: 'awg-resource-detail-json-raw',
templateUrl: './resource-detail-json-raw.component.html',
- styleUrls: ['./resource-detail-json-raw.component.css']
+ styleUrls: ['./resource-detail-json-raw.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class ResourceDetailJsonRawComponent implements OnInit {
+ /**
+ * Input variable: resourceJsonRawData.
+ *
+ * It keeps the raw json data for the resource detail.
+ */
@Input()
resourceJsonRawData: ResourceFullResponseJson;
- constructor() {}
-
+ /**
+ * Angular life cycle hook: ngOnInit.
+ *
+ * It calls the containing methods
+ * when initializing the component.
+ */
ngOnInit() {}
}
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.css b/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.css
index bf5434c93f..b105315b01 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.css
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.css
@@ -27,3 +27,7 @@
white-space: nowrap;
}
}
+
+ngb-tabset ::ng-deep .tab-pane.active {
+ margin: 2rem 0;
+}
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.html b/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.html
index 203f16cf42..2af6044f38 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.html
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.html
@@ -1,87 +1,77 @@
-
-
-
-
-
- {{ tabTitle?.html }}
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
+
+
+
+
Die Anfrage "{{ errorMessage?.route }}" ist fehlgeschlagen.
+
Fehlermeldung: "{{ errorMessage?.statusText || errorMessage }}".
+
Möglicherweise gab es ein Problem mit der Internetverbindung oder dem verwendeten Suchbegriff.
+
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+ {{ tabTitles?.html }}
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
Die Anfrage "{{ errorMessage?.route }}" ist fehlgeschlagen.
-
Fehlermeldung: "{{ errorMessage?.statusText }}".
-
Möglicherweise gab es ein Problem mit der Internetverbindung oder dem verwendeten Suchbegriff.
-
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.spec.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.spec.ts
index 2ea00c111e..081ec14865 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.spec.ts
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.spec.ts
@@ -1,14 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { Component, DebugElement, Input } from '@angular/core';
+import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
+
import { ActivatedRouteStub } from '@testing/router-stubs';
-import { NgbTabsetModule } from '@ng-bootstrap/ng-bootstrap';
+import { mockResourceDetail, mockResourceFullResponseJson } from '@testing/mock-data';
-import { ResourceDetail, ResourceDetailHeader } from '@awg-views/data-view/models';
-import { ResourceFullResponseJson } from '@awg-shared/api-objects';
+import { of as observableOf } from 'rxjs';
+import { JsonConvert } from 'json2typescript';
+import { NgbTabsetModule } from '@ng-bootstrap/ng-bootstrap';
+import Spy = jasmine.Spy;
+import { DataStreamerService, LoadingService } from '@awg-core/services';
import { DataApiService } from '@awg-views/data-view/services';
-import { ConversionService, DataStreamerService } from '@awg-core/services';
+
+import { ResourceFullResponseJson } from '@awg-shared/api-objects';
+import { ResourceData, ResourceDetail, ResourceDetailHeader } from '@awg-views/data-view/models';
import { ResourceDetailComponent } from './resource-detail.component';
@@ -19,16 +25,16 @@ class ResourceDetailHeaderStubComponent {
header: ResourceDetailHeader;
@Input()
resourceUrl: string;
-
- // TODO: handle outputs
+ @Output()
+ resourceRequest: EventEmitter = new EventEmitter();
}
@Component({ selector: 'awg-resource-detail-html', template: '' })
class ResourceDetailHtmlStubComponent {
@Input()
resourceDetailData: ResourceDetail;
-
- // TODO: handle outputs
+ @Output()
+ resourceRequest: EventEmitter = new EventEmitter();
}
@Component({ selector: 'awg-resource-detail-json-converted', template: '' })
@@ -43,21 +49,33 @@ class ResourceDetailJsonRawStubComponent {
resourceJsonRawData: ResourceFullResponseJson;
}
+@Component({ selector: 'awg-twelve-tone-spinner', template: '' })
+class TwelveToneSpinnerStubComponent {}
+
describe('ResourceDetailComponent', () => {
let component: ResourceDetailComponent;
let fixture: ComponentFixture;
let compDe: DebugElement;
let compEl: any;
- let mockRouter;
+ let mockRouter: Spy;
let mockActivatedRoute: ActivatedRouteStub;
+ // json object
+ let jsonConvert: JsonConvert;
+ let expectedResourceFullResponseJson: ResourceFullResponseJson;
+
+ let expectedResourceData: ResourceData;
+
beforeEach(async(() => {
// stub services for test purposes
// TODO: provide accurate types and service responses
- const mockConversionService = { prepareResourceDetail: () => {} };
- const mockSearchService = { httpGetUrl: '', getResourceDetailData: () => {} };
- const mockStreamerService = { updateCurrentResourceIdStream: () => {} };
+ const mockDataApiService = {
+ httpGetUrl: '/testUrl',
+ getResourceData: () => observableOf(expectedResourceData)
+ };
+ const mockLoadingService = { getLoadingStatus: () => observableOf(false) };
+ const mockDataStreamerService = { updateResourceId: () => {} };
// router spy object
mockRouter = jasmine.createSpyObj('Router', ['navigate']);
@@ -71,14 +89,15 @@ describe('ResourceDetailComponent', () => {
ResourceDetailHeaderStubComponent,
ResourceDetailHtmlStubComponent,
ResourceDetailJsonConvertedStubComponent,
- ResourceDetailJsonRawStubComponent
+ ResourceDetailJsonRawStubComponent,
+ TwelveToneSpinnerStubComponent
],
providers: [
- { provide: ConversionService, useValue: mockConversionService },
- { provide: DataApiService, useValue: mockSearchService },
- { provide: DataStreamerService, useValue: mockStreamerService },
+ { provide: ActivatedRoute, useValue: mockActivatedRoute },
{ provide: Router, useValue: mockRouter },
- { provide: ActivatedRoute, useValue: mockActivatedRoute }
+ { provide: DataApiService, useValue: mockDataApiService },
+ { provide: DataStreamerService, useValue: mockDataStreamerService },
+ { provide: LoadingService, useValue: mockLoadingService }
]
}).compileComponents();
}));
@@ -89,10 +108,18 @@ describe('ResourceDetailComponent', () => {
compDe = fixture.debugElement;
compEl = compDe.nativeElement;
- /*
- mockActivatedRoute.setParamMap({ id: '1234' });
- mockActivatedRoute.paramMap.subscribe(value => console.log(value));
- */
+ // mockActivatedRoute.setParamMap({ id: '1234' });
+ // mockActivatedRoute.paramMap.subscribe(value => console.log(value));
+
+ // convert json objects
+ jsonConvert = new JsonConvert();
+ expectedResourceFullResponseJson = jsonConvert.deserializeObject(
+ mockResourceFullResponseJson,
+ ResourceFullResponseJson
+ );
+
+ // test data
+ expectedResourceData = new ResourceData(expectedResourceFullResponseJson, mockResourceDetail);
fixture.detectChanges();
});
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.ts
index 3461e3a30c..97af269869 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.ts
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail.component.ts
@@ -1,144 +1,233 @@
-import { Component, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
-import { switchMap, map } from 'rxjs/operators';
+import { Observable, Subject } from 'rxjs';
+import { switchMap, takeUntil } from 'rxjs/operators';
import { NgbTabsetConfig } from '@ng-bootstrap/ng-bootstrap';
-import { ConversionService, DataStreamerService } from '@awg-core/services';
+import { DataStreamerService, LoadingService } from '@awg-core/services';
import { DataApiService } from '@awg-views/data-view/services';
-import { ResourceData, ResourceDetail } from '@awg-views/data-view/models';
-import { ResourceFullResponseJson } from '@awg-shared/api-objects';
+import { ResourceData } from '@awg-views/data-view/models';
+
+/**
+ * The ResourceDetail component.
+ *
+ * It contains the resource detail section
+ * of the data (search) view of the app
+ * with a {@link TwelveToneSpinnerComponent}
+ * and an ng-bootstrap tabset that contains
+ * the {@link ResourceDetailHeaderComponent},
+ * {@link ResourceDetailHtmlComponent},
+ * {@link ResourceDetailJsonConvertedComponent}
+ * and the {@link ResourceDetailJsonRawComponent}.
+ */
@Component({
selector: 'awg-resource-detail',
templateUrl: './resource-detail.component.html',
styleUrls: ['./resource-detail.component.css'],
- providers: [NgbTabsetConfig]
+ providers: [NgbTabsetConfig],
+ changeDetection: ChangeDetectionStrategy.Default
})
-export class ResourceDetailComponent implements OnInit {
- resourceData: ResourceData;
- resourceId: string;
- resourceUrl: string;
- oldId: string;
+export class ResourceDetailComponent implements OnInit, OnDestroy {
+ /**
+ * Public variable: destroy$.
+ *
+ * Subject to emit a truthy value in the ngOnDestroy lifecycle hook.
+ */
+ destroy$: Subject = new Subject();
+ /**
+ * Public variable: errorMessage.
+ *
+ * It keeps an errorMessage for the resource data subscription.
+ */
errorMessage: any = undefined;
- tabTitle = {
+ /**
+ * Public variable: oldId.
+ *
+ * It keeps the id of the previous resource detail.
+ */
+ oldId: string;
+
+ /**
+ * Public variable: resourceData.
+ *
+ * It keeps the data of the resource detail.
+ */
+ resourceData: ResourceData;
+
+ /**
+ * Public variable: resourceId.
+ *
+ * It keeps the id of the current resource detail.
+ */
+ resourceId: string;
+
+ /**
+ * Public variable: tabTitles.
+ *
+ * It keeps the titles for the tab panels.
+ */
+ tabTitles = {
html: 'Detail',
raw: 'JSON (raw)',
converted: 'JSON (converted)'
};
+ /**
+ * Getter for the httpGetUrl of the {@link DataApiService}.
+ */
+ get httpGetUrl(): string {
+ return this.dataApiService.httpGetUrl;
+ }
+
+ /**
+ * Getter for the loading status observable of the {@link LoadingService}.
+ */
+ get isLoading$(): Observable {
+ return this.loadingService.getLoadingStatus();
+ }
+
+ /**
+ * Constructor of the ResourceDetailComponent.
+ *
+ * It declares private instances of the Angular ActivatedRoute,
+ * the Angular Router, the DataApiService, the DataStreamerService,
+ * the LoadingService, and a configuration object for the
+ * ng-bootstrap tabset.
+ *
+ * @param {ActivatedRoute} route Instance of the Angular ActivatedRoute.
+ * @param {Router} router Instance of the Angular Router.
+ * @param {DataApiService} dataApiService Instance of the DataApiService.
+ * @param {DataStreamerService} dataStreamerService Instance of the DataStreamerService.
+ * @param {LoadingService} loadingService Instance of the LoadingService.
+ * @param {NgbTabsetConfig} config Instance of the NgbTabsetConfig.
+ */
constructor(
private route: ActivatedRoute,
private router: Router,
- private conversionService: ConversionService,
- private searchService: DataApiService,
- private streamerService: DataStreamerService,
+ private dataApiService: DataApiService,
+ private dataStreamerService: DataStreamerService,
+ private loadingService: LoadingService,
config: NgbTabsetConfig
) {
config.justify = 'justified';
}
- /*
- * Scroll to Top of Window
+ /**
+ * Angular life cycle hook: ngOnInit.
+ *
+ * It calls the containing methods
+ * when initializing the component.
*/
- static scrollToTop() {
- window.scrollTo(0, 0);
- }
-
ngOnInit() {
+ this.routeToSidenav();
this.getResourceData();
- this.activateSidenav();
}
- getResourceData() {
+ /**
+ * Public method: getResourceData.
+ *
+ * It gets the resource id from the route params
+ * and fetches the corresponding resource data
+ * from the {@link DataApiService}.
+ *
+ * @returns {void} Sets the resource data.
+ */
+ getResourceData(): void {
// observe route params
this.route.paramMap
.pipe(
switchMap((params: ParamMap) => {
- // store resource id
- this.resourceId = params.get('id');
-
- // fetch data
- return this.searchService.getResourceDetailData(params.get('id')).pipe(
- map((resourceBody: ResourceFullResponseJson) => {
- // update current resource params (url and id) via streamer service
- this.updateResourceParams();
-
- // prepare resource detail
- return this.prepareResourceDetail(resourceBody);
- })
- );
- })
+ // short cut for id param
+ const id = params.get('id');
+
+ // update current resource id via streamer service
+ this.updateResourceId(id);
+
+ // fetch resource data depending on param id
+ return this.dataApiService.getResourceData(id);
+ }),
+ takeUntil(this.destroy$)
)
.subscribe(
- (resourceData: ResourceData) => {
- this.resourceData = resourceData;
-
- // scroll to Top of Page
- ResourceDetailComponent.scrollToTop();
+ (data: ResourceData) => {
+ // subscribe to resource data to trigger loading service
+ this.resourceData = data;
},
- error => {
- this.errorMessage = error as any;
- }
+ err => (this.errorMessage = err)
);
}
- updateResourceParams() {
- // update current id
- this.updateResourceId();
-
- // update url for resource
- this.updateCurrentUrl();
- }
+ /**
+ * Public method: updateResourceId.
+ *
+ * It updates the resource id in the component
+ * and the streamer service.
+ *
+ * @param {string} id The given resource id.
+ *
+ * @returns {void} Sets the resource id.
+ */
+ updateResourceId(id: string): void {
+ // store current resource id
+ this.resourceId = id;
- updateResourceId() {
// share current id via streamer service
- this.streamerService.updateCurrentResourceIdStream(this.resourceId);
+ this.dataStreamerService.updateResourceId(id);
}
- updateCurrentUrl() {
- // get url from search service
- this.resourceUrl = this.searchService.httpGetUrl;
- }
-
- prepareResourceDetail(resourceBody: ResourceFullResponseJson): ResourceData {
- if (Object.keys(resourceBody).length === 0 && resourceBody.constructor === Object) {
- return;
- }
-
- // convert data for displaying resource detail
- const html: ResourceDetail = this.conversionService.prepareResourceDetail(resourceBody, this.resourceId);
-
- // return new resource data
- return (this.resourceData = new ResourceData(resourceBody, html));
- }
-
- /*
- * Navigate to ResourceDetail:
- * if nextId is emitted, use nextId for navigation, else navigate to oldId (backButton)
- * if oldId not exists (first call), use resourceId
+ /**
+ * Public method: navigateToResource.
+ *
+ * It navigates to the '/data/resource' route
+ * with the given id.
+ *
+ * If nextId is emitted, use nextId for navigation,
+ * else navigate to oldId (backButton). If oldId
+ * not exists (first call), use resourceId.
+ *
+ * @param {string} [id] The given resource id.
+ * @returns {void} Navigates to the resource.
*/
- navigateToResource(nextId?: string): void {
- const showId = nextId ? nextId : this.oldId ? this.oldId : this.resourceId;
+ navigateToResource(id?: string): void {
+ const nextId = id ? id : this.oldId ? this.oldId : this.resourceId;
+
// save resourceId as oldId
this.oldId = this.resourceId;
- // update resourceId
- this.resourceId = showId;
// navigate to new resource
- this.router.navigate(['/data/resource', +this.resourceId]);
+ this.router.navigate(['/data/resource', +nextId]);
}
- /*
- * Activate Sidenav: ResourceInfo
+ /**
+ * Public method: routeToSidenav.
+ *
+ * It activates the secondary outlet with the resource-info.
+ *
+ * @returns {void} Activates the resource-info side outlet.
*/
- activateSidenav(): void {
+ routeToSidenav(): void {
this.router.navigate([{ outlets: { side: 'resourceInfo' } }], {
preserveFragment: true,
queryParamsHandling: 'preserve'
});
}
+
+ /**
+ * Angular life cycle hook: ngOnDestroy.
+ *
+ * It calls the containing methods
+ * when destroying the component.
+ */
+ ngOnDestroy() {
+ // emit truthy value to end all subscriptions
+ this.destroy$.next(true);
+
+ // Now let's also unsubscribe from the subject itself:
+ this.destroy$.unsubscribe();
+ }
}
diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail.module.ts b/src/app/views/data-view/data-outlets/resource-detail/resource-detail.module.ts
index 4699edc5a2..cf0ea05186 100644
--- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail.module.ts
+++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail.module.ts
@@ -10,6 +10,12 @@ import { ResourceDetailHtmlContentPropsComponent } from './resource-detail-html/
import { ResourceDetailJsonConvertedComponent } from './resource-detail-json-converted/resource-detail-json-converted.component';
import { ResourceDetailJsonRawComponent } from './resource-detail-json-raw/resource-detail-json-raw.component';
+/**
+ * The resource detail module.
+ *
+ * It embeds the resource detail components
+ * as well as the {@link SharedModule}.
+ */
@NgModule({
imports: [SharedModule],
declarations: [
diff --git a/src/app/views/data-view/data-outlets/search-overview.component.spec.ts b/src/app/views/data-view/data-outlets/search-overview.component.spec.ts
index 58cd4900b6..61d74d0a76 100644
--- a/src/app/views/data-view/data-outlets/search-overview.component.spec.ts
+++ b/src/app/views/data-view/data-outlets/search-overview.component.spec.ts
@@ -1,19 +1,16 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/core';
+
import Spy = jasmine.Spy;
+import { expectSpyCall, getAndExpectDebugElementByDirective } from '@testing/expect-helper';
-import {
- expectSpyCall,
- getAndExpectDebugElementByCss,
- getAndExpectDebugElementByDirective
-} from '@testing/expect-helper';
import { RouterOutletStubComponent } from '@testing/router-stubs';
-
import { SideInfoService } from '@awg-core/services';
-import { RouterLinkButton } from '@awg-shared/router-link-button-group/router-link-button.model';
+import { RouterLinkButton } from '@awg-shared/router-link-button-group/router-link-button.model';
import { SearchOverviewComponent } from './search-overview.component';
+import { ActivatedRoute } from '@angular/router';
// mock components
@Component({ selector: 'awg-router-link-button-group', template: '' })
@@ -30,22 +27,57 @@ describe('SearchOverviewComponent (DONE)', () => {
let compDe: DebugElement;
let compEl: any;
- let expectedButtonArray: RouterLinkButton[];
+ let expectedButtonArray: RouterLinkButton[] = [
+ new RouterLinkButton('/data/search', 'fulltext', 'Volltext-Suche', false),
+ new RouterLinkButton('/data/search', 'timeline', 'Timeline', true),
+ new RouterLinkButton('/data/search', 'bibliography', 'Bibliographie', true)
+ ];
+
+ let mockActivatedRoute;
+ let mockActivatedRoutePath: string;
let selectButtonSpy: Spy;
- let updateSearchInfoTitleSpy: Spy;
+ let updateSearchInfoTitleFromPathSpy: Spy;
+ let service_updateSearchInfoTitleSpy: Spy;
+ let service_clearSearchInfoDataSpy: Spy;
beforeEach(async(() => {
// create a fake service object with a `updateSearchInfoTitle()` spy
- const sideInfoService = jasmine.createSpyObj('SideInfoService', ['updateSearchInfoTitle']);
+ const mockSideInfoService = jasmine.createSpyObj('SideInfoService', [
+ 'updateSearchInfoTitle',
+ 'clearSearchInfoData'
+ ]);
// spies on service
- updateSearchInfoTitleSpy = sideInfoService.updateSearchInfoTitle.and.callThrough();
+ service_updateSearchInfoTitleSpy = mockSideInfoService.updateSearchInfoTitle.and.callThrough();
+ service_clearSearchInfoDataSpy = mockSideInfoService.clearSearchInfoData.and.callThrough();
+
+ // mocked activated route
+ // see https://gist.github.com/benjamincharity/3d25cd2c95b6ecffadb18c3d4dbbd80b
+ mockActivatedRoute = {
+ snapshot: {
+ children: [
+ {
+ url: [
+ {
+ path: 'fulltext'
+ }
+ ]
+ }
+ ]
+ }
+ };
+ mockActivatedRoutePath = mockActivatedRoute.snapshot.children[0].url[0].path;
TestBed.configureTestingModule({
- imports: [],
declarations: [SearchOverviewComponent, RouterLinkButtonGroupStubComponent, RouterOutletStubComponent],
- providers: [{ provide: SideInfoService, useValue: sideInfoService }]
+ providers: [
+ { provide: SideInfoService, useValue: mockSideInfoService },
+ {
+ provide: ActivatedRoute,
+ useValue: mockActivatedRoute
+ }
+ ]
}).compileComponents();
}));
@@ -66,6 +98,7 @@ describe('SearchOverviewComponent (DONE)', () => {
// `.and.callThrough` will track the spy down the nested describes, see
// https://jasmine.github.io/2.0/introduction.html#section-Spies:_%3Ccode%3Eand.callThrough%3C/code%3E
selectButtonSpy = spyOn(component, 'onButtonSelect').and.callThrough();
+ updateSearchInfoTitleFromPathSpy = spyOn(component, 'updateSearchInfoTitleFromPath').and.callThrough();
});
it('should create', () => {
@@ -73,19 +106,32 @@ describe('SearchOverviewComponent (DONE)', () => {
});
describe('BEFORE initial data binding', () => {
- it('should not have `searchButtonArray`', () => {
- expect(component.searchButtonArray).toBeUndefined('should be undefined');
+ it('should have `searchButtonArray`', () => {
+ expect(component.searchButtonArray).toBeDefined('should be defined');
+ expect(component.searchButtonArray).toEqual(expectedButtonArray, `should equal ${expectedButtonArray}`);
});
- describe('#onButtonSelect', () => {
+ describe('#updateSearchInfoTitleFromPath', () => {
it('... should not have been called', () => {
- expect(component.onButtonSelect).not.toHaveBeenCalled();
+ expectSpyCall(updateSearchInfoTitleFromPathSpy, 0);
});
});
- describe('SideInfoService# updateSearchInfoTitle', () => {
+ describe('#onButtonSelect', () => {
it('... should not have been called', () => {
- expect(updateSearchInfoTitleSpy).not.toHaveBeenCalled();
+ expectSpyCall(selectButtonSpy, 0);
+ });
+
+ describe('SideInfoService# updateSearchInfoTitle', () => {
+ it('... should not have been called', () => {
+ expect(service_updateSearchInfoTitleSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('SideInfoService# clearSearchInfoData', () => {
+ it('... should not have been called', () => {
+ expect(service_clearSearchInfoDataSpy).not.toHaveBeenCalled();
+ });
});
});
@@ -106,16 +152,7 @@ describe('SearchOverviewComponent (DONE)', () => {
fixture.detectChanges();
});
- it('should have `searchButtonArray`', () => {
- expect(component.searchButtonArray).toBeDefined('should be defined');
- expect(component.searchButtonArray).toEqual(expectedButtonArray, `should equal ${expectedButtonArray}`);
- });
-
describe('VIEW', () => {
- it('... should contain one router outlet (stubbed)', () => {
- getAndExpectDebugElementByDirective(compDe, RouterOutletStubComponent, 1, 1);
- });
-
it('... should contain one RouterLinkButtonGroupComponent', () => {
getAndExpectDebugElementByDirective(compDe, RouterLinkButtonGroupStubComponent, 1, 1);
});
@@ -131,6 +168,29 @@ describe('SearchOverviewComponent (DONE)', () => {
});
});
+ describe('#updateSearchInfoTitleFromPath', () => {
+ it('... should have been called', () => {
+ expectSpyCall(updateSearchInfoTitleFromPathSpy, 1);
+ });
+
+ it('... should update search info title from path', () => {
+ expectSpyCall(updateSearchInfoTitleFromPathSpy, 1);
+
+ const path = mockActivatedRoutePath;
+
+ // filter searchButtonArray
+ const expectedButton = expectedButtonArray.filter(button => {
+ return button.link === path;
+ });
+
+ if (expectedButton.length === 1) {
+ expectSpyCall(service_updateSearchInfoTitleSpy, 1, expectedButton[0].label);
+ } else {
+ expectSpyCall(service_updateSearchInfoTitleSpy, 0);
+ }
+ });
+ });
+
describe('#onButtonSelect', () => {
it('... should not have been called', () => {
expect(component.onButtonSelect).not.toHaveBeenCalled();
@@ -158,63 +218,154 @@ describe('SearchOverviewComponent (DONE)', () => {
expectSpyCall(selectButtonSpy, 3, expectedButtonArray[2]);
}));
- it('... should not do anything if no RouterLinkButton provided', fakeAsync(() => {
- const buttonDes = getAndExpectDebugElementByDirective(compDe, RouterLinkButtonGroupStubComponent, 1, 1);
- const buttonCmp = buttonDes[0].injector.get(
- RouterLinkButtonGroupStubComponent
- ) as RouterLinkButtonGroupStubComponent;
+ describe('... should not do anything if no RouterLinkButton provided', () => {
+ // first call was on init
let noRouterLinkButton;
- // emit undefined
- noRouterLinkButton = undefined;
- buttonCmp.selectButtonRequest.emit(noRouterLinkButton);
-
- expectSpyCall(selectButtonSpy, 1, noRouterLinkButton);
- expect(updateSearchInfoTitleSpy).not.toHaveBeenCalled();
-
- // emit null
- noRouterLinkButton = null;
- buttonCmp.selectButtonRequest.emit(noRouterLinkButton);
-
- expectSpyCall(selectButtonSpy, 2, noRouterLinkButton);
- expect(updateSearchInfoTitleSpy).not.toHaveBeenCalled();
-
- // emit empty string
- noRouterLinkButton = '';
- buttonCmp.selectButtonRequest.emit(noRouterLinkButton);
-
- expectSpyCall(selectButtonSpy, 3, noRouterLinkButton);
- expect(updateSearchInfoTitleSpy).not.toHaveBeenCalled();
-
- // emit string
- noRouterLinkButton = 'test';
- buttonCmp.selectButtonRequest.emit(noRouterLinkButton);
+ it('... not with undefined', () => {
+ const buttonDes = getAndExpectDebugElementByDirective(
+ compDe,
+ RouterLinkButtonGroupStubComponent,
+ 1,
+ 1
+ );
+ const buttonCmp = buttonDes[0].injector.get(
+ RouterLinkButtonGroupStubComponent
+ ) as RouterLinkButtonGroupStubComponent;
+
+ // emit undefined
+ noRouterLinkButton = undefined;
+ buttonCmp.selectButtonRequest.emit(noRouterLinkButton);
+
+ expectSpyCall(selectButtonSpy, 1, noRouterLinkButton);
+ expectSpyCall(service_updateSearchInfoTitleSpy, 1);
+ });
+
+ it('... not with null', () => {
+ const buttonDes = getAndExpectDebugElementByDirective(
+ compDe,
+ RouterLinkButtonGroupStubComponent,
+ 1,
+ 1
+ );
+ const buttonCmp = buttonDes[0].injector.get(
+ RouterLinkButtonGroupStubComponent
+ ) as RouterLinkButtonGroupStubComponent;
+
+ // emit null
+ noRouterLinkButton = null;
+ buttonCmp.selectButtonRequest.emit(noRouterLinkButton);
+
+ expectSpyCall(selectButtonSpy, 1, noRouterLinkButton);
+ expectSpyCall(service_updateSearchInfoTitleSpy, 1);
+ });
+
+ it('... not with empty string', () => {
+ const buttonDes = getAndExpectDebugElementByDirective(
+ compDe,
+ RouterLinkButtonGroupStubComponent,
+ 1,
+ 1
+ );
+ const buttonCmp = buttonDes[0].injector.get(
+ RouterLinkButtonGroupStubComponent
+ ) as RouterLinkButtonGroupStubComponent;
+
+ // emit empty string
+ noRouterLinkButton = '';
+ buttonCmp.selectButtonRequest.emit(noRouterLinkButton);
+
+ expectSpyCall(selectButtonSpy, 1, noRouterLinkButton);
+ expectSpyCall(service_updateSearchInfoTitleSpy, 1);
+ });
+
+ it('... not with string', () => {
+ const buttonDes = getAndExpectDebugElementByDirective(
+ compDe,
+ RouterLinkButtonGroupStubComponent,
+ 1,
+ 1
+ );
+ const buttonCmp = buttonDes[0].injector.get(
+ RouterLinkButtonGroupStubComponent
+ ) as RouterLinkButtonGroupStubComponent;
+
+ // emit string
+ noRouterLinkButton = 'test';
+ buttonCmp.selectButtonRequest.emit(noRouterLinkButton);
+
+ expectSpyCall(selectButtonSpy, 1, noRouterLinkButton);
+ expectSpyCall(service_updateSearchInfoTitleSpy, 1);
+ });
+
+ it('... not with number', () => {
+ const buttonDes = getAndExpectDebugElementByDirective(
+ compDe,
+ RouterLinkButtonGroupStubComponent,
+ 1,
+ 1
+ );
+ const buttonCmp = buttonDes[0].injector.get(
+ RouterLinkButtonGroupStubComponent
+ ) as RouterLinkButtonGroupStubComponent;
+
+ // emit number
+ noRouterLinkButton = 12;
+ buttonCmp.selectButtonRequest.emit(noRouterLinkButton);
+
+ expectSpyCall(selectButtonSpy, 1, noRouterLinkButton);
+ expectSpyCall(service_updateSearchInfoTitleSpy, 1);
+ });
+
+ it('... not with router link button without label', () => {
+ const buttonDes = getAndExpectDebugElementByDirective(
+ compDe,
+ RouterLinkButtonGroupStubComponent,
+ 1,
+ 1
+ );
+ const buttonCmp = buttonDes[0].injector.get(
+ RouterLinkButtonGroupStubComponent
+ ) as RouterLinkButtonGroupStubComponent;
+
+ // emit router link button without label
+ noRouterLinkButton = new RouterLinkButton('/data/search', 'fulltext', undefined, false);
+ buttonCmp.selectButtonRequest.emit(noRouterLinkButton);
+
+ expectSpyCall(selectButtonSpy, 1, noRouterLinkButton);
+ expectSpyCall(service_updateSearchInfoTitleSpy, 1);
+ });
+ });
- expectSpyCall(selectButtonSpy, 4, noRouterLinkButton);
- expect(updateSearchInfoTitleSpy).not.toHaveBeenCalled();
+ it('... should call SideInfoService# clearSearchInfoData', fakeAsync(() => {
+ // emit button 1
+ component.onButtonSelect(expectedButtonArray[0]);
+ expectSpyCall(service_clearSearchInfoDataSpy, 1);
- // emit number
- noRouterLinkButton = 12;
- buttonCmp.selectButtonRequest.emit(noRouterLinkButton);
+ // emit button 2
+ component.onButtonSelect(expectedButtonArray[1]);
+ expectSpyCall(service_clearSearchInfoDataSpy, 2);
- expectSpyCall(selectButtonSpy, 5, noRouterLinkButton);
- expect(updateSearchInfoTitleSpy).not.toHaveBeenCalled();
+ // emit button 3
+ component.onButtonSelect(expectedButtonArray[2]);
+ expectSpyCall(service_clearSearchInfoDataSpy, 3);
}));
- it('... should update SideInfoService#updateSearchInfoTitle', fakeAsync(() => {
+ it('... should call SideInfoService# updateSearchInfoTitle', fakeAsync(() => {
+ // first call was on init
+
// emit button 1
component.onButtonSelect(expectedButtonArray[0]);
- expectSpyCall(updateSearchInfoTitleSpy, 1, expectedButtonArray[0].label);
+ expectSpyCall(service_updateSearchInfoTitleSpy, 2, expectedButtonArray[0].label);
// emit button 2
component.onButtonSelect(expectedButtonArray[1]);
- expectSpyCall(updateSearchInfoTitleSpy, 2, expectedButtonArray[1].label);
+ expectSpyCall(service_updateSearchInfoTitleSpy, 3, expectedButtonArray[1].label);
// emit button 3
- // trigger click with click helper & wait for changes
component.onButtonSelect(expectedButtonArray[2]);
- expectSpyCall(updateSearchInfoTitleSpy, 3, expectedButtonArray[2].label);
+ expectSpyCall(service_updateSearchInfoTitleSpy, 4, expectedButtonArray[2].label);
}));
});
});
diff --git a/src/app/views/data-view/data-outlets/search-overview.component.ts b/src/app/views/data-view/data-outlets/search-overview.component.ts
index e6c2664637..98635b983a 100644
--- a/src/app/views/data-view/data-outlets/search-overview.component.ts
+++ b/src/app/views/data-view/data-outlets/search-overview.component.ts
@@ -3,34 +3,97 @@ import { Component, OnInit } from '@angular/core';
import { RouterLinkButton } from '@awg-shared/router-link-button-group/router-link-button.model';
import { SideInfoService } from '@awg-core/services';
+import { ActivatedRoute } from '@angular/router';
+/**
+ * The SearchOverview component.
+ *
+ * It contains the overview section
+ * of the data (search) view of the app
+ * with a {@link RouterLinkButtonGroupComponent} and
+ * another router outlet for the data (search) routes.
+ */
@Component({
selector: 'awg-search-overview',
templateUrl: './search-overview.component.html',
styleUrls: ['./search-overview.component.css']
})
export class SearchOverviewComponent implements OnInit {
- searchButtonArray: RouterLinkButton[];
+ /**
+ * Public variable: searchButtonArray.
+ *
+ * It keeps the array for the search router link buttons.
+ */
+ searchButtonArray: RouterLinkButton[] = [
+ new RouterLinkButton('/data/search', 'fulltext', 'Volltext-Suche', false),
+ new RouterLinkButton('/data/search', 'timeline', 'Timeline', true),
+ new RouterLinkButton('/data/search', 'bibliography', 'Bibliographie', true)
+ ];
- constructor(private sideInfoService: SideInfoService) {}
+ /**
+ * Constructor of the SearchOverviewComponent.
+ *
+ * It declares a private SideInfoService instance
+ * to update the search info title and a private
+ * ActivatedRoute instance.
+ *
+ * @param {SideInfoService} sideInfoService Instance of the SideInfoService.
+ * @param {ActivatedRoute} route Instance of the ActivatedRoute.
+ */
+ constructor(private sideInfoService: SideInfoService, private route: ActivatedRoute) {}
+ /**
+ * Angular life cycle hook: ngOnInit.
+ *
+ * It calls the containing methods
+ * when initializing the component.
+ */
ngOnInit() {
- this.searchButtonArray = [
- new RouterLinkButton('/data/search', 'fulltext', 'Volltext-Suche', false),
- new RouterLinkButton('/data/search', 'timeline', 'Timeline', true),
- new RouterLinkButton('/data/search', 'bibliography', 'Bibliographie', true)
- ];
+ this.updateSearchInfoTitleFromPath();
}
- onButtonSelect(routerLinkButton: RouterLinkButton) {
- if (routerLinkButton && routerLinkButton instanceof RouterLinkButton) {
- this.updateSearchInfoTitle(routerLinkButton.label);
- } else {
- return;
+ /**
+ * Public method: updateSearchInfoTitleFromPath.
+ *
+ * It gets the current url path
+ * and sets the search info title
+ * from it via the SideInfoService.
+ *
+ * @returns {void} Updates the search info title.
+ */
+ updateSearchInfoTitleFromPath(): void {
+ // get snapshot from current url path
+ const path = this.route.snapshot.children[0].url[0].path;
+
+ // filter searchButtonArray
+ const selectedButton = this.searchButtonArray.filter(button => {
+ return button.link === path;
+ });
+
+ // update side info title if path is in array
+ if (selectedButton.length === 1) {
+ this.sideInfoService.updateSearchInfoTitle(selectedButton[0].label);
}
}
- private updateSearchInfoTitle(title: string) {
- this.sideInfoService.updateSearchInfoTitle(title);
+ /**
+ * Public method: onButtonSelect.
+ *
+ * It calls the updateSearchInfoTitle method to emit
+ * a selected button label to the SideInfoService.
+ *
+ * @param {RouterLinkButton} routerLinkButton
+ * The given router link button.
+ *
+ * @returns {void} Updates the search info title.
+ */
+ onButtonSelect(routerLinkButton: RouterLinkButton): void {
+ const isButton = routerLinkButton instanceof RouterLinkButton;
+
+ if (!routerLinkButton || !isButton || !routerLinkButton.label) {
+ return;
+ }
+ this.sideInfoService.clearSearchInfoData();
+ this.sideInfoService.updateSearchInfoTitle(routerLinkButton.label);
}
}
diff --git a/src/app/views/data-view/data-outlets/search-panel/search-form/search-form.component.html b/src/app/views/data-view/data-outlets/search-panel/search-form/search-form.component.html
index c799bc1fbe..a529fb861e 100644
--- a/src/app/views/data-view/data-outlets/search-panel/search-form/search-form.component.html
+++ b/src/app/views/data-view/data-outlets/search-panel/search-form/search-form.component.html
@@ -8,19 +8,22 @@
-
+
-
+
+
{{ searchFormStrings.errorMessage }}
-
diff --git a/src/app/views/data-view/data-outlets/search-panel/search-form/search-form.component.spec.ts b/src/app/views/data-view/data-outlets/search-panel/search-form/search-form.component.spec.ts
index 247a74b133..36b4302c12 100644
--- a/src/app/views/data-view/data-outlets/search-panel/search-form/search-form.component.spec.ts
+++ b/src/app/views/data-view/data-outlets/search-panel/search-form/search-form.component.spec.ts
@@ -1,7 +1,8 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
-import { ReactiveFormsModule } from '@angular/forms';
+import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
+
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { SearchFormComponent } from './search-form.component';
@@ -12,10 +13,17 @@ describe('SearchFormComponent', () => {
let compDe: DebugElement;
let compEl: any;
+ // create new instance of FormBuilder
+ // see 'Karma formGroup expects a FormGroup instance. Please pass one in',
+ // https://medium.com/@charlesprobaker/karma-testing-a-formgroup-instance-a0a90de831d4
+ // https://stackoverflow.com/a/48671534
+ const formBuilder: FormBuilder = new FormBuilder();
+
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [FontAwesomeModule, ReactiveFormsModule],
- declarations: [SearchFormComponent]
+ declarations: [SearchFormComponent],
+ providers: [{ provide: FormBuilder, useValue: formBuilder }]
}).compileComponents();
}));
@@ -25,6 +33,11 @@ describe('SearchFormComponent', () => {
compDe = fixture.debugElement;
compEl = compDe.nativeElement;
+ // pass in the form dynamically
+ component.searchForm = formBuilder.group({
+ searchValueControl: ['', Validators.compose([Validators.required, Validators.minLength(3)])]
+ });
+
fixture.detectChanges();
});
diff --git a/src/app/views/data-view/data-outlets/search-panel/search-form/search-form.component.ts b/src/app/views/data-view/data-outlets/search-panel/search-form/search-form.component.ts
index 5f265a06e0..fe777a7f8c 100644
--- a/src/app/views/data-view/data-outlets/search-panel/search-form/search-form.component.ts
+++ b/src/app/views/data-view/data-outlets/search-panel/search-form/search-form.component.ts
@@ -1,69 +1,153 @@
-import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
-import { distinctUntilChanged, debounceTime, filter } from 'rxjs/operators';
+import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
import { faSearch } from '@fortawesome/free-solid-svg-icons';
+/**
+ * The SearchForm component.
+ *
+ * It contains the search form section
+ * of the data (search) view of the app
+ * with a reactive form group.
+ */
@Component({
selector: 'awg-search-form',
templateUrl: './search-form.component.html',
- styleUrls: ['./search-form.component.css']
+ styleUrls: ['./search-form.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class SearchFormComponent implements OnChanges {
+ /**
+ * Input variable: searchValue.
+ *
+ * It keeps the search value from the search panel parent.
+ */
@Input()
searchValue: string;
+
+ /**
+ * Output variable: searchRequest.
+ *
+ * It keeps an event emitter for the search string.
+ */
@Output()
searchRequest: EventEmitter = new EventEmitter();
+ /**
+ * Public variable: faSearch.
+ *
+ * It instantiates fontawesome's faSearch icon.
+ */
faSearch = faSearch;
+ /**
+ * Public variable: searchForm.
+ *
+ * It keeps the reactive form group: searchForm.
+ */
searchForm: FormGroup;
+
+ /**
+ * Public variable: searchFormString.
+ *
+ * It keeps the default texts for the search form.
+ */
searchFormStrings = {
label: 'Search Input',
placeholder: 'Volltextsuche in der Webern-Datenbank …',
errorMessage: 'Es wird ein Suchbegriff mit mindestens 3 Zeichen benötigt!'
};
- constructor(private fb: FormBuilder) {
- // building form in constructor is necessary
- // to check for input changes in ngOnChanges
- this.buildForm(this.searchValue);
+ /**
+ * Getter for the search value control value.
+ */
+ get searchValueControl() {
+ return this.searchForm.get('searchValueControl');
}
- // check for input changes
- ngOnChanges(changes: SimpleChanges) {
- if (changes['searchValue']) {
- // set input value to search form
- this.searchForm.patchValue({ searchValueControl: this.searchValue });
- }
+ /**
+ * Constructor of the SearchFormComponent.
+ *
+ * It declares a private FormBuilder instance
+ * and initializes the form group.
+ *
+ * @param {FormBuilder} formBuilder Instance of the FormBuilder.
+ */
+ constructor(private formBuilder: FormBuilder) {
+ this.createFormGroup();
+ this.listenToUserInputChange();
}
- // build search form
- buildForm(searchValue: string) {
- this.searchForm = this.fb.group({
- searchValueControl: [searchValue || '', Validators.compose([Validators.required, Validators.minLength(3)])]
- });
+ /**
+ * Angular life cycle hook: ngOnChanges.
+ *
+ * It checks for changes of the given input.
+ */
+ ngOnChanges() {
+ this.setSearchValueFromInput();
+ }
- this.checkForUserInputChange();
+ /**
+ * Public method: createFormGroup.
+ *
+ * It creates the search form using the reactive FormBuilder
+ * with a formGroup and a search value control.
+ *
+ * @returns {void} Creates the search form.
+ */
+ createFormGroup(): void {
+ this.searchForm = this.formBuilder.group({
+ searchValueControl: ['', Validators.compose([Validators.required, Validators.minLength(3)])]
+ });
}
- // check for changing search values
- checkForUserInputChange(): void {
- this.searchForm
- .get('searchValueControl')
- .valueChanges.pipe(
+ /**
+ * Public method: listenToUserInputChange.
+ *
+ * It listens to the user's input changes
+ * in the search value control and triggers
+ * the onSearch method with the new search value.
+ *
+ * @returns {void} Listens to changing search values.
+ */
+ listenToUserInputChange(): void {
+ this.searchValueControl.valueChanges
+ .pipe(
+ // at least 3 characters
filter(x => x.length >= 3),
+ // do not check changes before half a second
debounceTime(500),
+ // do not check unchanged values
distinctUntilChanged()
)
- .subscribe((query: string) => {
- this.onSearch(query);
- });
+ .subscribe((query: string) => this.onSearch(query));
+ }
+
+ /**
+ * Public method: setSearchValueFromInput.
+ *
+ * It sets the value of the search value control
+ * from the searchValue input.
+ *
+ * Needed to catch search values given in the URL.
+ *
+ * @returns {void} Sets the search value control.
+ */
+ setSearchValueFromInput(): void {
+ this.searchValueControl.setValue(this.searchValue);
}
- // emit query to search panel
- onSearch(query: string) {
+ /**
+ * Public method: onSearch.
+ *
+ * It emits a search query to the {@link searchRequest}.
+ *
+ * @param {string} query The given search query.
+ * @returns {void} Emits the search query.
+ */
+ onSearch(query: string): void {
this.searchRequest.emit(query);
}
}
diff --git a/src/app/views/data-view/data-outlets/search-panel/search-panel.component.html b/src/app/views/data-view/data-outlets/search-panel/search-panel.component.html
index a3acfce670..fea27f2741 100644
--- a/src/app/views/data-view/data-outlets/search-panel/search-panel.component.html
+++ b/src/app/views/data-view/data-outlets/search-panel/search-panel.component.html
@@ -1,33 +1,26 @@
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
Die Anfrage "{{ errorMessage?.route }}" ist fehlgeschlagen.
Fehlermeldung: "{{ errorMessage?.statusText || errorMessage }}".
Möglicherweise gab es ein Problem mit der Internetverbindung oder dem verwendeten Suchbegriff.
+
+
+
diff --git a/src/app/views/data-view/data-outlets/search-panel/search-panel.component.spec.ts b/src/app/views/data-view/data-outlets/search-panel/search-panel.component.spec.ts
index df9bb754bf..56ca3f7cdf 100644
--- a/src/app/views/data-view/data-outlets/search-panel/search-panel.component.spec.ts
+++ b/src/app/views/data-view/data-outlets/search-panel/search-panel.component.spec.ts
@@ -1,19 +1,25 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { Component, DebugElement, Input } from '@angular/core';
+import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/core';
+import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
-import { SearchPanelComponent } from './search-panel.component';
+import { of as observableOf } from 'rxjs';
+
+import { ActivatedRouteStub } from '@testing/router-stubs';
+
import { DataApiService } from '@awg-views/data-view/services';
-import { ConversionService, DataStreamerService } from '@awg-core/services';
+import { ConversionService, DataStreamerService, LoadingService } from '@awg-core/services';
import { SearchParams } from '@awg-views/data-view/models';
+import { SearchPanelComponent } from './search-panel.component';
+
@Component({ selector: 'awg-search-form', template: '' })
class SearchFormStubComponent {
@Input()
searchValue: string;
-
- // TODO: handle outputs
+ @Output()
+ searchRequest: EventEmitter = new EventEmitter();
}
@Component({ selector: 'awg-search-result-list', template: '' })
@@ -22,6 +28,12 @@ class SearchResultListStubComponent {
searchParams: SearchParams;
@Input()
searchUrl: string;
+ @Output()
+ pageChangeRequest: EventEmitter = new EventEmitter();
+ @Output()
+ rowChangeRequest: EventEmitter = new EventEmitter();
+ @Output()
+ viewChangeRequest: EventEmitter = new EventEmitter();
}
@Component({ selector: 'awg-twelve-tone-spinner', template: '' })
@@ -33,10 +45,23 @@ describe('SearchPanelComponent', () => {
let compDe: DebugElement;
let compEl: any;
+ let mockRouter;
+ let mockActivatedRoute: ActivatedRouteStub;
+
// stub services for test purposes
const mocConversionService = { convertFullTextSearchResults: () => {} };
- const mockdataApiService = { httpGetUrl: '/testUrl', getFulltextSearchData: () => {} };
- const mockStreamerService = { updateSearchResponseStream: () => {} };
+ const mockDataApiService = { httpGetUrl: '/testUrl', getFulltextSearchData: () => observableOf({}) };
+ const mockLoadingService = { getLoadingStatus: () => observableOf(false) };
+ const mockDataStreamerService = { updateSearchResponseWithQuery: () => {} };
+
+ // router spy object
+ mockRouter = {
+ url: '/test-url',
+ events: observableOf(new NavigationEnd(0, 'http://localhost:4200/test-url', 'http://localhost:4200/test-url')),
+ navigate: jasmine.createSpy('navigate')
+ };
+ // mocked activated route
+ mockActivatedRoute = new ActivatedRouteStub();
beforeEach(async(() => {
TestBed.configureTestingModule({
@@ -48,9 +73,12 @@ describe('SearchPanelComponent', () => {
TwelveToneSpinnerStubComponent
],
providers: [
+ { provide: ActivatedRoute, useValue: mockActivatedRoute },
+ { provide: Router, useValue: mockRouter },
{ provide: ConversionService, useValue: mocConversionService },
- { provide: DataApiService, useValue: mockdataApiService },
- { provide: DataStreamerService, useValue: mockStreamerService }
+ { provide: DataApiService, useValue: mockDataApiService },
+ { provide: DataStreamerService, useValue: mockDataStreamerService },
+ { provide: LoadingService, useValue: mockLoadingService }
]
}).compileComponents();
}));
diff --git a/src/app/views/data-view/data-outlets/search-panel/search-panel.component.ts b/src/app/views/data-view/data-outlets/search-panel/search-panel.component.ts
index ec8dab4b1c..b6c7fd7903 100644
--- a/src/app/views/data-view/data-outlets/search-panel/search-panel.component.ts
+++ b/src/app/views/data-view/data-outlets/search-panel/search-panel.component.ts
@@ -1,126 +1,276 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, NavigationEnd, ParamMap, Router } from '@angular/router';
-import { Subscription } from 'rxjs';
+import { Observable, Subject, Subscription } from 'rxjs';
+import { filter, map, takeUntil } from 'rxjs/operators';
-import { ConversionService, DataStreamerService } from '@awg-core/services';
+import { ConversionService, DataStreamerService, LoadingService } from '@awg-core/services';
import { DataApiService } from '@awg-views/data-view/services';
import { SearchResponseJson } from '@awg-shared/api-objects';
-import { SearchParams, SearchResponseWithQuery } from '@awg-views/data-view/models';
-
+import { SearchParams, SearchParamsViewTypes, SearchResponseWithQuery } from '@awg-views/data-view/models';
+
+/**
+ * The SearchPanel component.
+ *
+ * It contains the search panel section
+ * of the data (search) view of the app
+ * with a {@link TwelveToneSpinnerComponent},
+ * the {@link SearchFormComponent}
+ * and the {@link SearchResultListComponent}.
+ */
@Component({
selector: 'awg-search-panel',
templateUrl: './search-panel.component.html',
styleUrls: ['./search-panel.component.css']
})
export class SearchPanelComponent implements OnInit, OnDestroy {
- navigationSubscription: Subscription;
-
- searchUrl = '';
+ /**
+ * Public variable: destroy$.
+ *
+ * Subject to emit a truthy value in the ngOnDestroy lifecycle hook.
+ */
+ destroy$: Subject = new Subject();
+
+ /**
+ * Public variable: currentQueryParams.
+ *
+ * It keeps the current ParamMap from the query url.
+ */
currentQueryParams: ParamMap;
+ /**
+ * Public variable: searchParams.
+ *
+ * It keeps the default parameters for the search.
+ */
searchParams: SearchParams = {
query: '',
- nRows: '10',
+ nRows: '25',
startAt: '0',
- view: 'table'
+ view: SearchParamsViewTypes.table
};
- errorMessage: any;
- isLoadingData = false;
+ /**
+ * Getter for the httpGetUrl of the {@link DataApiService}.
+ */
+ get httpGetUrl(): string {
+ return this.dataApiService.httpGetUrl;
+ }
+
+ /**
+ * Getter for the loading status observable of the {@link LoadingService}.
+ */
+ get isLoading$(): Observable {
+ return this.loadingService.getLoadingStatus();
+ }
+
+ /**
+ * Public variable: errorMessage.
+ *
+ * It keeps an errorMessage for the search response subscription.
+ */
+ errorMessage: any = undefined;
+
+ /**
+ * Public variable: viewChanged.
+ *
+ * If the view has changed.
+ */
viewChanged = false;
+ /**
+ * Constructor of the SearchPanelComponent.
+ *
+ * It declares private instances of the Angular ActivatedRoute,
+ * the Angular Router, the ConversionService, the DataApiService,
+ * the DataStreamerService, and the LoadingService.
+ *
+ * @param {ActivatedRoute} route Instance of the Angular ActivatedRoute.
+ * @param {Router} router Instance of the Angular Router.
+ * @param {ConversionService} conversionService Instance of the ConversionService.
+ * @param {DataApiService} dataApiService Instance of the DataApiService.
+ * @param {DataStreamerService} dataStreamerService Instance of the DataStreamerService.
+ * @param {LoadingService} loadingService Instance of the LoadingService.
+ */
constructor(
private route: ActivatedRoute,
private router: Router,
private conversionService: ConversionService,
private dataApiService: DataApiService,
- private streamerService: DataStreamerService
+ private dataStreamerService: DataStreamerService,
+ private loadingService: LoadingService
) {}
+ /**
+ * Angular life cycle hook: ngOnInit.
+ *
+ * It calls the containing methods
+ * when initializing the component.
+ */
ngOnInit() {
- this.navigationSubscription = this.subscribeToDataApiService();
+ this.getFulltextSearchData();
}
- // switch the load status
- changeLoadingStatus(status: boolean) {
- this.isLoadingData = status;
- }
-
- // start loading activities
- onLoadingStart(): void {
- this.changeLoadingStatus(true);
- }
+ /**
+ * Public method: getFulltextSearchData.
+ *
+ * It gets the query parameters from the route's query params
+ * and fetches the corresponding fulltext search data
+ * from the {@link DataApiService}.
+ *
+ * @returns {void} Sets the search data.
+ *
+ * @todo Refactor nested subscription.
+ */
+ getFulltextSearchData(): void {
+ this.router.events.pipe(takeUntil(this.destroy$)).subscribe(
+ (e: any) => {
+ // check for end of navigation
+ if (e instanceof NavigationEnd) {
+ // snapshot of current route query params
+ const qp = this.route.snapshot.queryParamMap;
+
+ if (qp !== this.currentQueryParams) {
+ this.currentQueryParams = qp;
+
+ if (qp.keys.length < 4) {
+ // update search params
+ this.updateSearchParamsFromRoute(qp, true);
+ } else {
+ // update search params from route if available
+ this.updateSearchParamsFromRoute(qp, false);
- // end loading activities
- onLoadingEnd(): void {
- this.changeLoadingStatus(false);
+ if (this.searchParams.query && !this.viewChanged) {
+ // fetch search data
+ return this.dataApiService.getFulltextSearchData(this.searchParams).subscribe(
+ (searchResponse: SearchResponseJson) => {
+ // share search data via streamer service
+ const searchResponseWithQuery: SearchResponseWithQuery = new SearchResponseWithQuery(
+ searchResponse,
+ this.searchParams.query
+ );
+ this.dataStreamerService.updateSearchResponseWithQuery(searchResponseWithQuery);
+ },
+ error => {
+ this.errorMessage = error as any;
+ }
+ );
+ } else {
+ // console.log('No search query!');
+ }
+ }
+ } else {
+ // console.log('Routed on same page with same query params');
+ }
+ }
+ },
+ error => {
+ this.errorMessage = error as any;
+ }
+ );
}
- // new startPosition after page change request
+ /**
+ * Public method: onPageChange.
+ *
+ * It sets a new start position value in the searchParams
+ * after a page change request and triggers the
+ * {@link routeToSelf} method.
+ *
+ * @param {string} requestedStartAt The given start position.
+ *
+ * @returns {void} Sets the search params and routes to itself.
+ */
onPageChange(requestedStartAt: string): void {
if (requestedStartAt !== this.searchParams.startAt) {
// view has not changed
this.viewChanged = false;
- const sp: SearchParams = {
+ this.searchParams = {
query: this.searchParams.query,
nRows: this.searchParams.nRows,
startAt: requestedStartAt,
view: this.searchParams.view
};
// route to new params
- this.routeToSelf(sp);
+ this.routeToSelf(this.searchParams);
}
}
- // new row number after row change request
- onRowChange(requestedRows: string): void {
+ /**
+ * Public method: onRowNumberChange.
+ *
+ * It sets new row number value in the searchParams
+ * after a row change request and triggers the
+ * {@link routeToSelf} method.
+ *
+ * @param {string} requestedRows The given row.
+ *
+ * @returns {void} Sets the search params and routes to itself.
+ */
+ onRowNumberChange(requestedRows: string): void {
if (requestedRows !== this.searchParams.nRows) {
// view has not changed
this.viewChanged = false;
- // reset start position
- this.searchParams.startAt = '0';
-
- const sp: SearchParams = {
+ this.searchParams = {
query: this.searchParams.query,
nRows: requestedRows,
- startAt: this.searchParams.startAt,
+ startAt: '0',
view: this.searchParams.view
};
// route to new params
- this.routeToSelf(sp);
+ this.routeToSelf(this.searchParams);
}
}
- // new row number after row change request
+ /**
+ * Public method: onViewChange.
+ *
+ * It sets new view type value in the searchParams
+ * after a view change request and triggers the
+ * {@link routeToSelf} method.
+ *
+ * @param {string} requestedView The given view.
+ *
+ * @returns {void} Sets the search params and routes to itself.
+ */
onViewChange(requestedView: string): void {
if (requestedView !== this.searchParams.view) {
// view has changed
this.viewChanged = true;
- const sp: SearchParams = {
+ this.searchParams = {
query: this.searchParams.query,
nRows: this.searchParams.nRows,
startAt: this.searchParams.startAt,
- view: requestedView
+ view: SearchParamsViewTypes[requestedView]
};
// route to new params
- this.routeToSelf(sp);
+ this.routeToSelf(this.searchParams);
}
}
- // new query after search request
+ /**
+ * Public method: onSearch.
+ *
+ * It sets new query value in the searchParams
+ * after a search request and triggers the
+ * {@link routeToSelf} method.
+ *
+ * @param {string} requestedQuery The given search query.
+ *
+ * @returns {void} Sets the search params and routes to itself.
+ */
onSearch(requestedQuery: string): void {
if (requestedQuery !== this.searchParams.query) {
// view has not changed
this.viewChanged = false;
- const sp: SearchParams = {
+ this.searchParams = {
query: requestedQuery,
nRows: this.searchParams.nRows,
startAt: this.searchParams.startAt,
@@ -128,11 +278,19 @@ export class SearchPanelComponent implements OnInit, OnDestroy {
};
// route to new search params
- this.routeToSelf(sp);
+ this.routeToSelf(this.searchParams);
}
}
- // route to self to set new params
+ /**
+ * Public method: routeToSelf.
+ *
+ * It navigates to itself to set
+ * new search parameters.
+ *
+ * @param {SearchParams} sp The given search parameters.
+ * @returns {void} Navigates to itself.
+ */
routeToSelf(sp: SearchParams) {
this.router.navigate([], {
relativeTo: this.route,
@@ -141,104 +299,48 @@ export class SearchPanelComponent implements OnInit, OnDestroy {
});
}
- subscribeToDataApiService(): Subscription {
- return this.router.events.subscribe((e: any) => {
- // check for end of navigation
- if (e instanceof NavigationEnd) {
- // snapshot of current route query params
- const qp = this.route.snapshot.queryParamMap;
-
- if (qp !== this.currentQueryParams) {
- this.currentQueryParams = qp;
-
- if (qp.keys.length < 4) {
- const sp: SearchParams = {
- query: qp.get('query') || this.searchParams.query,
- nRows: qp.get('nrows') || this.searchParams.nRows,
- startAt: qp.get('startAt') || this.searchParams.startAt,
- view: qp.get('view') || this.searchParams.view
- };
- this.routeToSelf(sp);
- } else {
- // update search params from route if available
- this.updateSearchParamsFromRoute(qp);
-
- if (this.searchParams.query && !this.viewChanged) {
- // start loading
- this.onLoadingStart();
-
- // fetch search data
- return this.dataApiService
- .getFulltextSearchData(
- this.searchParams.query,
- this.searchParams.nRows,
- this.searchParams.startAt
- )
- .subscribe(
- (searchResponse: SearchResponseJson) => {
- // update url for search
- this.updateCurrentUrl();
-
- // share search data via streamer service
- this.updateStreamerService(searchResponse, this.searchParams.query);
-
- // end loading
- this.onLoadingEnd();
- },
- error => {
- this.errorMessage = error as any;
- }
- );
- } else {
- // console.log('No search query!');
- }
- }
- } else {
- // console.log('Routed on same page with same query params');
- }
- }
- });
- }
-
- updateCurrentUrl() {
- // get url from search service
- this.searchUrl = this.dataApiService.httpGetUrl;
- }
-
- // update search params from route
- updateSearchParamsFromRoute(params: ParamMap) {
+ /**
+ * Public method: updateSearchParamsFromRoute.
+ *
+ * It updates the searchParams from the query params
+ * of the current route and routes to the component
+ * itself again if 'routing' is truthy.
+ *
+ * @param {ParamMap} params The given url query params.
+ * @param {boolean} routing If the search parameters should be set
+ * by routing to the component itself again.
+ *
+ * @returns {void} Sets the search params and routes to itself if routing is truthy.
+ */
+ updateSearchParamsFromRoute(params: ParamMap, routing: boolean) {
if (!params) {
return;
}
- if (params.get('query') && params.get('query') !== this.searchParams.query) {
- this.searchParams.query = params.get('query');
- }
- if (params.get('nrows') && params.get('nrows') !== this.searchParams.nRows) {
- this.searchParams.nRows = params.get('nrows');
- }
- if (params.get('startAt') && params.get('startAt') !== this.searchParams.startAt) {
- this.searchParams.startAt = params.get('startAt');
- }
- if (
- params.get('view') &&
- (params.get('view') === 'table' || params.get('view') === 'grid') &&
- params.get('view') !== this.searchParams.view
- ) {
- this.searchParams.view = params.get('view');
- }
- }
+ // update search params (immutable)
+ this.searchParams = {
+ query: params.get('query') || this.searchParams.query,
+ nRows: params.get('nrows') || this.searchParams.nRows,
+ startAt: params.get('startAt') || this.searchParams.startAt,
+ view: SearchParamsViewTypes[params.get('view')] || this.searchParams.view
+ };
- // update search data via streamer service
- updateStreamerService(searchResponse: SearchResponseJson, query: string) {
- const searchResponseWithQuery: SearchResponseWithQuery = new SearchResponseWithQuery(searchResponse, query);
- this.streamerService.updateSearchResponseStream(searchResponseWithQuery);
+ if (routing) {
+ this.routeToSelf(this.searchParams);
+ }
}
+ /**
+ * Angular life cycle hook: ngOnDestroy.
+ *
+ * It calls the containing methods
+ * when destroying the component.
+ */
ngOnDestroy() {
- // prevent memory leak when component destroyed
- if (this.navigationSubscription) {
- this.navigationSubscription.unsubscribe();
- }
+ // emit truthy value to end all subscriptions
+ this.destroy$.next(true);
+
+ // Now let's also unsubscribe from the subject itself:
+ this.destroy$.unsubscribe();
}
}
diff --git a/src/app/views/data-view/data-outlets/search-panel/search-result-list/search-result-list.component.html b/src/app/views/data-view/data-outlets/search-panel/search-result-list/search-result-list.component.html
index 0122a6d524..b7331d3e4a 100644
--- a/src/app/views/data-view/data-outlets/search-panel/search-result-list/search-result-list.component.html
+++ b/src/app/views/data-view/data-outlets/search-panel/search-result-list/search-result-list.component.html
@@ -1,28 +1,49 @@
-