mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'iiif-mirador' of https://github.com/mspalti/dspace-angular into iiif-mirador
This commit is contained in:
92
.github/workflows/build.yml
vendored
92
.github/workflows/build.yml
vendored
@@ -16,6 +16,9 @@ jobs:
|
|||||||
DSPACE_REST_PORT: 8080
|
DSPACE_REST_PORT: 8080
|
||||||
DSPACE_REST_NAMESPACE: '/server'
|
DSPACE_REST_NAMESPACE: '/server'
|
||||||
DSPACE_REST_SSL: false
|
DSPACE_REST_SSL: false
|
||||||
|
# When Chrome version is specified, we pin to a specific version of Chrome
|
||||||
|
# Comment this out to use the latest release
|
||||||
|
#CHROME_VERSION: "90.0.4430.212-1"
|
||||||
strategy:
|
strategy:
|
||||||
# Create a matrix of Node versions to test against (in parallel)
|
# Create a matrix of Node versions to test against (in parallel)
|
||||||
matrix:
|
matrix:
|
||||||
@@ -34,10 +37,20 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
- name: Install latest Chrome (for e2e tests)
|
# If CHROME_VERSION env variable specified above, then pin to that version.
|
||||||
|
# Otherwise, just install latest version of Chrome.
|
||||||
|
- name: Install Chrome (for e2e tests)
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
if [[ -z "${CHROME_VERSION}" ]]
|
||||||
sudo apt-get --only-upgrade install google-chrome-stable -y
|
then
|
||||||
|
echo "Installing latest stable version"
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get --only-upgrade install google-chrome-stable -y
|
||||||
|
else
|
||||||
|
echo "Installing version ${CHROME_VERSION}"
|
||||||
|
wget -q "https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb"
|
||||||
|
sudo dpkg -i "google-chrome-stable_${CHROME_VERSION}_amd64.deb"
|
||||||
|
fi
|
||||||
google-chrome --version
|
google-chrome --version
|
||||||
|
|
||||||
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
|
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
|
||||||
@@ -53,9 +66,6 @@ jobs:
|
|||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
restore-keys: ${{ runner.os }}-yarn-
|
restore-keys: ${{ runner.os }}-yarn-
|
||||||
|
|
||||||
- name: Install the latest chromedriver compatible with the installed chrome version
|
|
||||||
run: yarn global add chromedriver --detect_chromedriver_version
|
|
||||||
|
|
||||||
- name: Install Yarn dependencies
|
- name: Install Yarn dependencies
|
||||||
run: yarn install --frozen-lockfile
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
@@ -83,23 +93,63 @@ jobs:
|
|||||||
docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
|
docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
|
||||||
docker container ls
|
docker container ls
|
||||||
|
|
||||||
# Wait until the REST API returns a 200 response (or for a max of 30 seconds)
|
# Run integration tests via Cypress.io
|
||||||
# https://github.com/nev7n/wait_for_response
|
# https://github.com/cypress-io/github-action
|
||||||
- name: Wait for DSpace REST Backend to be ready (for e2e tests)
|
# (NOTE: to run these e2e tests locally, just use 'ng e2e')
|
||||||
uses: nev7n/wait_for_response@v1
|
|
||||||
with:
|
|
||||||
# We use the 'sites' endpoint to also ensure the database is ready
|
|
||||||
url: 'http://localhost:8080/server/api/core/sites'
|
|
||||||
responseCode: 200
|
|
||||||
timeout: 30000
|
|
||||||
|
|
||||||
- name: Get DSpace REST Backend info/properties
|
|
||||||
run: curl http://localhost:8080/server/api
|
|
||||||
|
|
||||||
- name: Run e2e tests (integration tests)
|
- name: Run e2e tests (integration tests)
|
||||||
|
uses: cypress-io/github-action@v2
|
||||||
|
with:
|
||||||
|
# Run tests in Chrome, headless mode
|
||||||
|
browser: chrome
|
||||||
|
headless: true
|
||||||
|
# Start app before running tests (will be stopped automatically after tests finish)
|
||||||
|
start: yarn run serve:ssr
|
||||||
|
# Wait for backend & frontend to be available
|
||||||
|
# NOTE: We use the 'sites' REST endpoint to also ensure the database is ready
|
||||||
|
wait-on: http://localhost:8080/server/api/core/sites, http://localhost:4000
|
||||||
|
# Wait for 2 mins max for everything to respond
|
||||||
|
wait-on-timeout: 120
|
||||||
|
|
||||||
|
# Cypress always creates a video of all e2e tests (whether they succeeded or failed)
|
||||||
|
# Save those in an Artifact
|
||||||
|
- name: Upload e2e test videos to Artifacts
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: e2e-test-videos
|
||||||
|
path: cypress/videos
|
||||||
|
|
||||||
|
# If e2e tests fail, Cypress creates a screenshot of what happened
|
||||||
|
# Save those in an Artifact
|
||||||
|
- name: Upload e2e test failure screenshots to Artifacts
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
name: e2e-test-screenshots
|
||||||
|
path: cypress/screenshots
|
||||||
|
|
||||||
|
# Start up the app with SSR enabled (run in background)
|
||||||
|
- name: Start app in SSR (server-side rendering) mode
|
||||||
run: |
|
run: |
|
||||||
chromedriver --url-base='/wd/hub' --port=4444 &
|
nohup yarn run serve:ssr &
|
||||||
yarn run e2e:ci
|
printf 'Waiting for app to start'
|
||||||
|
until curl --output /dev/null --silent --head --fail http://localhost:4000/home; do
|
||||||
|
printf '.'
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "App started successfully."
|
||||||
|
|
||||||
|
# Get homepage and verify that the <meta name="title"> tag includes "DSpace".
|
||||||
|
# If it does, then SSR is working, as this tag is created by our MetadataService.
|
||||||
|
# This step also prints entire HTML of homepage for easier debugging if grep fails.
|
||||||
|
- name: Verify SSR (server-side rendering)
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://localhost:4000/home)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep DSpace
|
||||||
|
|
||||||
|
- name: Stop running app
|
||||||
|
run: kill -9 $(lsof -t -i:4000)
|
||||||
|
|
||||||
- name: Shutdown Docker containers
|
- name: Shutdown Docker containers
|
||||||
run: docker-compose -f ./docker/docker-compose-ci.yml down
|
run: docker-compose -f ./docker/docker-compose-ci.yml down
|
||||||
|
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
DSpace source code BSD License:
|
DSpace source code BSD License:
|
||||||
|
|
||||||
Copyright (c) 2002-2020, LYRASIS. All rights reserved.
|
Copyright (c) 2002-2021, LYRASIS. All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
modification, are permitted provided that the following conditions are
|
modification, are permitted provided that the following conditions are
|
||||||
|
210
README.md
210
README.md
@@ -3,12 +3,34 @@
|
|||||||
dspace-angular
|
dspace-angular
|
||||||
==============
|
==============
|
||||||
|
|
||||||
> The next UI for DSpace 7, based on Angular Universal.
|
> The DSpace User Interface built on [Angular](https://angular.io/), written in [TypeScript](https://www.typescriptlang.org/) and using [Angular Universal](https://angular.io/guide/universal).
|
||||||
|
|
||||||
This project is currently under active development. For more information on the DSpace 7 release see the [DSpace 7.0 Release Status wiki page](https://wiki.lyrasis.org/display/DSPACE/DSpace+Release+7.0+Status)
|
Overview
|
||||||
|
--------
|
||||||
|
|
||||||
You can find additional information on the DSpace 7 Angular UI on the [wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+-+Angular+UI+Development).
|
DSpace open source software is a turnkey repository application used by more than
|
||||||
|
2,000 organizations and institutions worldwide to provide durable access to digital resources.
|
||||||
|
For more information, visit http://www.dspace.org/
|
||||||
|
|
||||||
|
DSpace consists of both a Java-based backend and an Angular-based frontend.
|
||||||
|
|
||||||
|
* Backend (https://github.com/DSpace/DSpace/) provides a REST API, along with other machine-based interfaces (e.g. OAI-PMH, SWORD, etc)
|
||||||
|
* The REST Contract is at https://github.com/DSpace/RestContract
|
||||||
|
* Frontend (this codebase) is the User Interface built on the REST API
|
||||||
|
|
||||||
|
Downloads
|
||||||
|
---------
|
||||||
|
|
||||||
|
* Backend (REST API): https://github.com/DSpace/DSpace/releases
|
||||||
|
* Frontend (User Interface): https://github.com/DSpace/dspace-angular/releases
|
||||||
|
|
||||||
|
|
||||||
|
## Documentation / Installation
|
||||||
|
|
||||||
|
Documentation for each release may be viewed online or downloaded via our [Documentation Wiki](https://wiki.lyrasis.org/display/DSDOC/).
|
||||||
|
|
||||||
|
The latest DSpace Installation instructions are available at:
|
||||||
|
https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
|
||||||
|
|
||||||
Quick start
|
Quick start
|
||||||
-----------
|
-----------
|
||||||
@@ -39,14 +61,17 @@ Table of Contents
|
|||||||
- [Introduction to the technology](#introduction-to-the-technology)
|
- [Introduction to the technology](#introduction-to-the-technology)
|
||||||
- [Requirements](#requirements)
|
- [Requirements](#requirements)
|
||||||
- [Installing](#installing)
|
- [Installing](#installing)
|
||||||
- [Configuring](#configuring)
|
- [Configuring](#configuring)
|
||||||
- [Running the app](#running-the-app)
|
- [Running the app](#running-the-app)
|
||||||
- [Running in production mode](#running-in-production-mode)
|
- [Running in production mode](#running-in-production-mode)
|
||||||
- [Deploy](#deploy)
|
- [Deploy](#deploy)
|
||||||
- [Running the application with Docker](#running-the-application-with-docker)
|
- [Running the application with Docker](#running-the-application-with-docker)
|
||||||
- [Cleaning](#cleaning)
|
- [Cleaning](#cleaning)
|
||||||
- [Testing](#testing)
|
- [Testing](#testing)
|
||||||
- [Test a Pull Request](#test-a-pull-request)
|
- [Test a Pull Request](#test-a-pull-request)
|
||||||
|
- [Unit Tests](#unit-tests)
|
||||||
|
- [E2E Tests](#e2e-tests)
|
||||||
|
- [Writing E2E Tests](#writing-e2e-tests)
|
||||||
- [Documentation](#documentation)
|
- [Documentation](#documentation)
|
||||||
- [Other commands](#other-commands)
|
- [Other commands](#other-commands)
|
||||||
- [Recommended Editors/IDEs](#recommended-editorsides)
|
- [Recommended Editors/IDEs](#recommended-editorsides)
|
||||||
@@ -82,9 +107,9 @@ Default configuration file is located in `src/environments/` folder.
|
|||||||
To change the default configuration values, create local files that override the parameters you need to change. You can use `environment.template.ts` as a starting point.
|
To change the default configuration values, create local files that override the parameters you need to change. You can use `environment.template.ts` as a starting point.
|
||||||
|
|
||||||
- Create a new `environment.dev.ts` file in `src/environments/` for a `development` environment;
|
- Create a new `environment.dev.ts` file in `src/environments/` for a `development` environment;
|
||||||
- Create a new `environment.prod.ts` file in `src/environments/` for a `production` environment;
|
- Create a new `environment.prod.ts` file in `src/environments/` for a `production` environment;
|
||||||
|
|
||||||
The server settings can also be overwritten using an environment file.
|
The server settings can also be overwritten using an environment file.
|
||||||
|
|
||||||
This file should be called `.env` and be placed in the project root.
|
This file should be called `.env` and be placed in the project root.
|
||||||
|
|
||||||
@@ -103,7 +128,7 @@ DSPACE_REST_SSL # Whether the angular REST uses SSL [true/false]
|
|||||||
```
|
```
|
||||||
|
|
||||||
The same settings can also be overwritten by setting system environment variables instead, E.g.:
|
The same settings can also be overwritten by setting system environment variables instead, E.g.:
|
||||||
```bash
|
```bash
|
||||||
export DSPACE_HOST=api7.dspace.org
|
export DSPACE_HOST=api7.dspace.org
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -118,7 +143,7 @@ To use environment variables in a UI component, use:
|
|||||||
import { environment } from '../environment.ts';
|
import { environment } from '../environment.ts';
|
||||||
```
|
```
|
||||||
|
|
||||||
This file is generated by the script located in `scripts/set-env.ts`. This script will run automatically before every build, or can be manually triggered using the appropriate `config` script in `package.json`
|
This file is generated by the script located in `scripts/set-env.ts`. This script will run automatically before every build, or can be manually triggered using the appropriate `config` script in `package.json`
|
||||||
|
|
||||||
|
|
||||||
Running the app
|
Running the app
|
||||||
@@ -146,6 +171,9 @@ This will build the application and put the result in the `dist` folder. You ca
|
|||||||
|
|
||||||
|
|
||||||
### Running the application with Docker
|
### Running the application with Docker
|
||||||
|
NOTE: At this time, we do not have production-ready Docker images for DSpace.
|
||||||
|
That said, we do have quick-start Docker Compose scripts for development or testing purposes.
|
||||||
|
|
||||||
See [Docker Runtime Options](docker/README.md)
|
See [Docker Runtime Options](docker/README.md)
|
||||||
|
|
||||||
|
|
||||||
@@ -192,26 +220,50 @@ Place your tests in the same location of the application source code files that
|
|||||||
|
|
||||||
and run: `yarn run test`
|
and run: `yarn run test`
|
||||||
|
|
||||||
### E2E test
|
### E2E Tests
|
||||||
|
|
||||||
E2E tests use Protractor + Selenium server + browsers. You can find the configuration file at the same level of this README file:`./protractor.conf.js` Protractor is installed as 'local' as a dev dependency.
|
E2E tests (aka integration tests) use [Cypress.io](https://www.cypress.io/). Configuration for cypress can be found in the `cypress.json` file in the root directory.
|
||||||
|
|
||||||
If you are going to use a remote test enviroment you need to edit the './e2e//protractor.conf.js'. Follow the instructions you will find inside it.
|
The test files can be found in the `./cypress/integration/` folder.
|
||||||
|
|
||||||
The default browser is Google Chrome.
|
Before you can run e2e tests, you MUST have a running backend (i.e. REST API). By default, the e2e tests look for this at http://localhost:8080/server/ or whatever `rest` backend is defined in your `environment.prod.ts` or `environment.common.ts`. You may override this using env variables, see [Configuring](#configuring).
|
||||||
|
|
||||||
Place your tests at the following path: `./e2e`
|
Run `ng e2e` to kick off the tests. This will start Cypress and allow you to select the browser you wish to use, as well as whether you wish to run all tests or an individual test file. Once you click run on test(s), this opens the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) to run your test(s) and show you the results.
|
||||||
|
|
||||||
and run: `ng e2e`
|
#### Writing E2E Tests
|
||||||
|
|
||||||
### Continuous Integration (CI) Test
|
All E2E tests must be created under the `./cypress/integration/` folder, and must end in `.spec.ts`. Subfolders are allowed.
|
||||||
|
|
||||||
To run all the tests (e.g.: to run tests with Continuous Integration software) you can execute:`yarn run ci` Keep in mind that this command prerequisites are the sum of unit test and E2E tests.
|
* The easiest way to start creating new tests is by running `ng e2e`. This builds the app and brings up Cypress.
|
||||||
|
* From here, if you are editing an existing test file, you can either open it in your IDE or run it first to see what it already does.
|
||||||
|
* To create a new test file, click `+ New Spec File`. Choose a meaningful name ending in `spec.ts` (Please make sure it ends in `.ts` so that it's a Typescript file, and not plain Javascript)
|
||||||
|
* Start small. Add a basic `describe` and `it` which just [cy.visit](https://docs.cypress.io/api/commands/visit) the page you want to test. For example:
|
||||||
|
```
|
||||||
|
describe('Community/Collection Browse Page', () => {
|
||||||
|
it('should exist as a page', () => {
|
||||||
|
cy.visit('/community-list');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
* Run your test file from the Cypress window. This starts the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) in a new browser window.
|
||||||
|
* In the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner), you'll Cypress automatically visit the page. This first test will succeed, as all you are doing is making sure the _page exists_.
|
||||||
|
* From here, you can use the [Selector Playground](https://docs.cypress.io/guides/core-concepts/test-runner#Selector-Playground) in the Cypress Test Runner window to determine how to tell Cypress to interact with a specific HTML element on that page.
|
||||||
|
* Most commands start by telling Cypress to [get()](https://docs.cypress.io/api/commands/get) a specific element, using a CSS or jQuery style selector
|
||||||
|
* Cypress can then do actions like [click()](https://docs.cypress.io/api/commands/click) an element, or [type()](https://docs.cypress.io/api/commands/type) text in an input field, etc.
|
||||||
|
* Cypress can also validate that something occurs, using [should()](https://docs.cypress.io/api/commands/should) assertions.
|
||||||
|
* Any time you save your test file, the Cypress Test Runner will reload & rerun it. This allows you can see your results quickly as you write the tests & correct any broken tests rapidly.
|
||||||
|
* Cypress also has a great guide on [writing your first test](https://on.cypress.io/writing-first-test) with much more info. Keep in mind, while the examples in the Cypress docs often involve Javascript files (.js), the same examples will work in our Typescript (.ts) e2e tests.
|
||||||
|
|
||||||
|
_Hint: Creating e2e tests is easiest in an IDE (like Visual Studio), as it can help prompt/autocomplete your Cypress commands._
|
||||||
|
|
||||||
|
More Information: [docs.cypress.io](https://docs.cypress.io/) has great guides & documentation helping you learn more about writing/debugging e2e tests in Cypress.
|
||||||
|
|
||||||
Documentation
|
Documentation
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
See [`./docs`](docs) for further documentation.
|
Official DSpace documentation is available in the DSpace wiki at https://wiki.lyrasis.org/display/DSDOC7x/
|
||||||
|
|
||||||
|
Some UI specific configuration documentation is also found in the [`./docs`](docs) folder of htis codebase.
|
||||||
|
|
||||||
### Building code documentation
|
### Building code documentation
|
||||||
|
|
||||||
@@ -234,8 +286,6 @@ To get the most out of TypeScript, you'll need a TypeScript-aware editor. We've
|
|||||||
- Free
|
- Free
|
||||||
- [Visual Studio Code](https://code.visualstudio.com/)
|
- [Visual Studio Code](https://code.visualstudio.com/)
|
||||||
- [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome)
|
- [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome)
|
||||||
- [Atom](https://atom.io/)
|
|
||||||
- [TypeScript plugin](https://atom.io/packages/atom-typescript)
|
|
||||||
- Paid
|
- Paid
|
||||||
- [Webstorm](https://www.jetbrains.com/webstorm/download/) or [IntelliJ IDEA Ultimate](https://www.jetbrains.com/idea/)
|
- [Webstorm](https://www.jetbrains.com/webstorm/download/) or [IntelliJ IDEA Ultimate](https://www.jetbrains.com/idea/)
|
||||||
- [Sublime Text](http://www.sublimetext.com/3)
|
- [Sublime Text](http://www.sublimetext.com/3)
|
||||||
@@ -257,95 +307,43 @@ dspace-angular
|
|||||||
│ ├── environment.default.js * Default configuration files
|
│ ├── environment.default.js * Default configuration files
|
||||||
│ └── environment.test.js * Test configuration files
|
│ └── environment.test.js * Test configuration files
|
||||||
├── docs * Folder for documentation
|
├── docs * Folder for documentation
|
||||||
├── e2e * Folder for e2e test files
|
├── cypress * Folder for Cypress (https://cypress.io/) / e2e tests
|
||||||
│ ├── app.e2e-spec.ts *
|
│ ├── integration * Folder for e2e/integration test files
|
||||||
│ ├── app.po.ts *
|
│ ├── fixtures * Folder for any fixtures needed by e2e tests
|
||||||
│ ├── pagenotfound *
|
│ ├── plugins * Folder for Cypress plugins (if any)
|
||||||
│ │ ├── pagenotfound.e2e-spec.ts *
|
│ ├── support * Folder for global e2e test actions/commands (run for all tests)
|
||||||
│ │ └── pagenotfound.po.ts *
|
|
||||||
│ └── tsconfig.json * TypeScript configuration file for e2e tests
|
│ └── tsconfig.json * TypeScript configuration file for e2e tests
|
||||||
├── karma.conf.js * Karma configuration file for Unit Test
|
├── karma.conf.js * Karma configuration file for Unit Test
|
||||||
├── nodemon.json * Nodemon (https://nodemon.io/) configuration
|
├── nodemon.json * Nodemon (https://nodemon.io/) configuration
|
||||||
├── package.json * This file describes the npm package for this project, its dependencies, scripts, etc.
|
├── package.json * This file describes the npm package for this project, its dependencies, scripts, etc.
|
||||||
├── postcss.config.js * PostCSS (http://postcss.org/) configuration file
|
├── postcss.config.js * PostCSS (http://postcss.org/) configuration file
|
||||||
├── protractor.conf.js *
|
|
||||||
├── resources * Folder for static resources
|
|
||||||
│ ├── data * Folder for static data
|
|
||||||
│ │ └── en * Folder for i18n English data
|
|
||||||
│ ├── i18n * Folder for i18n translations
|
|
||||||
│ │ └── en.json * i18n translations for English
|
|
||||||
│ └── images * Folder for images
|
|
||||||
│ ├── dspace-logo-old.png *
|
|
||||||
│ ├── dspace-logo.png *
|
|
||||||
│ └── favicon.ico *
|
|
||||||
├── rollup.config.js * Rollup (http://rollupjs.org/) configuration
|
|
||||||
├── spec-bundle.js *
|
|
||||||
├── src * The source of the application
|
├── src * The source of the application
|
||||||
│ ├── app *
|
│ ├── app * The source code of the application, subdivided by module/page.
|
||||||
│ │ ├── app-routing.module.ts *
|
│ ├── assets * Folder for static resources
|
||||||
│ │ ├── app.component.html *
|
│ │ ├── fonts * Folder for fonts
|
||||||
│ │ ├── app.component.scss *
|
│ │ ├── i18n * Folder for i18n translations
|
||||||
│ │ ├── app.component.spec.ts *
|
│ | └── en.json5 * i18n translations for English
|
||||||
│ │ ├── app.component.ts *
|
│ │ └── images * Folder for images
|
||||||
│ │ ├── app.effects.ts *
|
|
||||||
│ │ ├── app.module.ts *
|
|
||||||
│ │ ├── app.reducer.ts *
|
|
||||||
│ │ ├── browser-app.module.ts * The root module for the client
|
|
||||||
│ │ ├── +collection-page * Lazily loaded route for collection module
|
|
||||||
│ │ ├── +community-page * Lazily loaded route for community module
|
|
||||||
│ │ ├── core *
|
|
||||||
│ │ ├── header *
|
|
||||||
│ │ ├── +home * Lazily loaded route for home module
|
|
||||||
│ │ ├── +item-page * Lazily loaded route for item module
|
|
||||||
│ │ ├── object-list *
|
|
||||||
│ │ ├── pagenotfound *
|
|
||||||
│ │ ├── server-app.module.ts * The root module for the server
|
|
||||||
│ │ ├── shared *
|
|
||||||
│ │ ├── store.actions.ts *
|
|
||||||
│ │ ├── store.effects.ts *
|
|
||||||
│ │ ├── thumbnail *
|
|
||||||
│ │ └── typings.d.ts * File that allows you to add custom typings for libraries without TypeScript support
|
|
||||||
│ ├── backend * Folder containing a mock of the REST API, hosted by the express server
|
│ ├── backend * Folder containing a mock of the REST API, hosted by the express server
|
||||||
│ │ ├── api.ts *
|
|
||||||
│ │ ├── cache.ts *
|
|
||||||
│ │ ├── data *
|
|
||||||
│ │ └── db.ts *
|
|
||||||
│ ├── config *
|
│ ├── config *
|
||||||
│ │ ├── cache-config.interface.ts *
|
|
||||||
│ │ ├── config.interface.ts *
|
|
||||||
│ │ ├── global-config.interface.ts *
|
|
||||||
│ │ ├── server-config.interface.ts *
|
|
||||||
│ │ └── universal-config.interface.ts *
|
|
||||||
│ ├── config.ts * File that loads environmental and shareable settings and makes them available to app components
|
|
||||||
│ ├── index.csr.html * The index file for client side rendering fallback
|
│ ├── index.csr.html * The index file for client side rendering fallback
|
||||||
│ ├── index.html * The index file
|
│ ├── index.html * The index file
|
||||||
│ ├── main.browser.ts * The bootstrap file for the client
|
│ ├── main.browser.ts * The bootstrap file for the client
|
||||||
│ ├── main.server.ts * The express (http://expressjs.com/) config and bootstrap file for the server
|
│ ├── main.server.ts * The express (http://expressjs.com/) config and bootstrap file for the server
|
||||||
|
│ ├── robots.txt * The robots.txt file
|
||||||
│ ├── modules *
|
│ ├── modules *
|
||||||
│ │ ├── cookies *
|
|
||||||
│ │ ├── data-loader *
|
|
||||||
│ │ ├── transfer-http *
|
|
||||||
│ │ ├── transfer-state *
|
|
||||||
│ │ ├── transfer-store *
|
|
||||||
│ │ └── translate-universal-loader.ts *
|
|
||||||
│ ├── routes.ts * The routes file for the server
|
|
||||||
│ ├── styles * Folder containing global styles
|
│ ├── styles * Folder containing global styles
|
||||||
│ │ ├── _mixins.scss *
|
│ └── themes * Folder containing available themes
|
||||||
│ │ └── variables.scss * Global sass variables file
|
│ ├── custom * Template folder for creating a custom theme
|
||||||
│ ├── tsconfig.browser.json * TypeScript config for the client build
|
│ └── dspace * Default 'dspace' theme
|
||||||
│ ├── tsconfig.server.json * TypeScript config for the server build
|
|
||||||
│ └── tsconfig.test.json * TypeScript config for the test build
|
|
||||||
├── tsconfig.json * TypeScript config
|
├── tsconfig.json * TypeScript config
|
||||||
├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration
|
├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration
|
||||||
├── typedoc.json * TYPEDOC configuration
|
├── typedoc.json * TYPEDOC configuration
|
||||||
├── webpack * Webpack (https://webpack.github.io/) config directory
|
├── webpack * Webpack (https://webpack.github.io/) config directory
|
||||||
│ ├── webpack.aot.js * Webpack (https://webpack.github.io/) config for AoT build
|
│ ├── webpack.browser.ts * Webpack (https://webpack.github.io/) config for client build
|
||||||
│ ├── webpack.client.js * Webpack (https://webpack.github.io/) config for client build
|
│ ├── webpack.common.ts *
|
||||||
│ ├── webpack.common.js *
|
│ ├── webpack.prod.ts * Webpack (https://webpack.github.io/) config for production build
|
||||||
│ ├── webpack.prod.js * Webpack (https://webpack.github.io/) config for production build
|
│ └── webpack.test.ts * Webpack (https://webpack.github.io/) config for test build
|
||||||
│ ├── webpack.server.js * Webpack (https://webpack.github.io/) config for server build
|
|
||||||
│ └── webpack.test.js * Webpack (https://webpack.github.io/) config for test build
|
|
||||||
├── webpack.config.ts *
|
|
||||||
└── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock)
|
└── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -403,8 +401,8 @@ Frequently asked questions
|
|||||||
- You can write your tests next to your component files. e.g. for `src/app/home/home.component.ts` call it `src/app/home/home.component.spec.ts`
|
- You can write your tests next to your component files. e.g. for `src/app/home/home.component.ts` call it `src/app/home/home.component.spec.ts`
|
||||||
- How do I start the app when I get `EACCES` and `EADDRINUSE` errors?
|
- How do I start the app when I get `EACCES` and `EADDRINUSE` errors?
|
||||||
- The `EADDRINUSE` error means the port `4000` is currently being used and `EACCES` is lack of permission to build files to `./dist/`
|
- The `EADDRINUSE` error means the port `4000` is currently being used and `EACCES` is lack of permission to build files to `./dist/`
|
||||||
- What are the naming conventions for Angular 2?
|
- What are the naming conventions for Angular?
|
||||||
- See [the official angular 2 style guide](https://angular.io/styleguide)
|
- See [the official angular style guide](https://angular.io/styleguide)
|
||||||
- Why is the size of my app larger in development?
|
- Why is the size of my app larger in development?
|
||||||
- The production build uses a whole host of techniques (ahead-of-time compilation, rollup to remove unreachable code, minification, etc.) to reduce the size, that aren't used during development in the intrest of build speed.
|
- The production build uses a whole host of techniques (ahead-of-time compilation, rollup to remove unreachable code, minification, etc.) to reduce the size, that aren't used during development in the intrest of build speed.
|
||||||
- node-pre-gyp ERR in yarn install (Windows)
|
- node-pre-gyp ERR in yarn install (Windows)
|
||||||
@@ -415,6 +413,32 @@ Frequently asked questions
|
|||||||
- then run `git add yarn.lock` to stage the lockfile for commit
|
- then run `git add yarn.lock` to stage the lockfile for commit
|
||||||
- and `git commit` to conclude the merge
|
- and `git commit` to conclude the merge
|
||||||
|
|
||||||
|
Getting Help
|
||||||
|
------------
|
||||||
|
|
||||||
|
DSpace provides public mailing lists where you can post questions or raise topics for discussion.
|
||||||
|
We welcome everyone to participate in these lists:
|
||||||
|
|
||||||
|
* [dspace-community@googlegroups.com](https://groups.google.com/d/forum/dspace-community) : General discussion about DSpace platform, announcements, sharing of best practices
|
||||||
|
* [dspace-tech@googlegroups.com](https://groups.google.com/d/forum/dspace-tech) : Technical support mailing list. See also our guide for [How to troubleshoot an error](https://wiki.lyrasis.org/display/DSPACE/Troubleshoot+an+error).
|
||||||
|
* [dspace-devel@googlegroups.com](https://groups.google.com/d/forum/dspace-devel) : Developers / Development mailing list
|
||||||
|
|
||||||
|
Great Q&A is also available under the [DSpace tag on Stackoverflow](http://stackoverflow.com/questions/tagged/dspace)
|
||||||
|
|
||||||
|
Additional support options are at https://wiki.lyrasis.org/display/DSPACE/Support
|
||||||
|
|
||||||
|
DSpace also has an active service provider network. If you'd rather hire a service provider to
|
||||||
|
install, upgrade, customize or host DSpace, then we recommend getting in touch with one of our
|
||||||
|
[Registered Service Providers](http://www.dspace.org/service-providers).
|
||||||
|
|
||||||
|
|
||||||
|
Issue Tracker
|
||||||
|
-------------
|
||||||
|
|
||||||
|
DSpace uses GitHub to track issues:
|
||||||
|
* Backend (REST API) issues: https://github.com/DSpace/DSpace/issues
|
||||||
|
* Frontend (User Interface) issues: https://github.com/DSpace/dspace-angular/issues
|
||||||
|
|
||||||
License
|
License
|
||||||
-------
|
-------
|
||||||
This project's source code is made available under the DSpace BSD License: http://www.dspace.org/license
|
This project's source code is made available under the DSpace BSD License: http://www.dspace.org/license
|
||||||
|
15
SECURITY.md
Normal file
15
SECURITY.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
For information regarding which versions of DSpace are currently under support, please see our DSpace Software Support Policy:
|
||||||
|
|
||||||
|
https://wiki.lyrasis.org/display/DSPACE/DSpace+Software+Support+Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you believe you have found a security vulnerability in a supported version of DSpace, we encourage you to let us know right away.
|
||||||
|
We will investigate all legitimate reports and do our best to quickly fix the problem. Please see our DSpace Software Support Policy
|
||||||
|
for information on privately reporting vulnerabilities:
|
||||||
|
|
||||||
|
https://wiki.lyrasis.org/display/DSPACE/DSpace+Software+Support+Policy
|
30
angular.json
30
angular.json
@@ -47,6 +47,7 @@
|
|||||||
"src/robots.txt"
|
"src/robots.txt"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
"src/styles/startup.scss",
|
||||||
{
|
{
|
||||||
"input": "src/styles/base-theme.scss",
|
"input": "src/styles/base-theme.scss",
|
||||||
"inject": false,
|
"inject": false,
|
||||||
@@ -146,7 +147,7 @@
|
|||||||
"tsConfig": [
|
"tsConfig": [
|
||||||
"tsconfig.app.json",
|
"tsconfig.app.json",
|
||||||
"tsconfig.spec.json",
|
"tsconfig.spec.json",
|
||||||
"e2e/tsconfig.json"
|
"cypress/tsconfig.json"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"**/node_modules/**"
|
"**/node_modules/**"
|
||||||
@@ -154,10 +155,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"e2e": {
|
"e2e": {
|
||||||
"builder": "@angular-devkit/build-angular:protractor",
|
"builder": "@cypress/schematic:cypress",
|
||||||
"options": {
|
"options": {
|
||||||
"protractorConfig": "e2e/protractor.conf.js",
|
"devServerTarget": "dspace-angular:serve",
|
||||||
"devServerTarget": "dspace-angular:serve"
|
"watch": true,
|
||||||
|
"headless": false
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@@ -214,9 +216,27 @@
|
|||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {}
|
"production": {}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cypress-run": {
|
||||||
|
"builder": "@cypress/schematic:cypress",
|
||||||
|
"options": {
|
||||||
|
"devServerTarget": "dspace-angular:serve"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"devServerTarget": "dspace-angular:serve:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cypress-open": {
|
||||||
|
"builder": "@cypress/schematic:cypress",
|
||||||
|
"options": {
|
||||||
|
"watch": true,
|
||||||
|
"headless": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultProject": "dspace-angular"
|
"defaultProject": "dspace-angular"
|
||||||
}
|
}
|
9
cypress.json
Normal file
9
cypress.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"integrationFolder": "cypress/integration",
|
||||||
|
"supportFile": "cypress/support/index.ts",
|
||||||
|
"videosFolder": "cypress/videos",
|
||||||
|
"screenshotsFolder": "cypress/screenshots",
|
||||||
|
"pluginsFile": "cypress/plugins/index.ts",
|
||||||
|
"fixturesFolder": "cypress/fixtures",
|
||||||
|
"baseUrl": "http://localhost:4000"
|
||||||
|
}
|
5
cypress/fixtures/example.json
Normal file
5
cypress/fixtures/example.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "Using fixtures to represent data",
|
||||||
|
"email": "hello@cypress.io",
|
||||||
|
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||||
|
}
|
37
cypress/integration/homepage.spec.ts
Normal file
37
cypress/integration/homepage.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
describe('Homepage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// All tests start with visiting homepage
|
||||||
|
cy.visit('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display translated title "DSpace Angular :: Home"', () => {
|
||||||
|
cy.title().should('eq', 'DSpace Angular :: Home');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a news section', () => {
|
||||||
|
cy.get('ds-home-news').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have a working search box', () => {
|
||||||
|
const queryString = 'test';
|
||||||
|
cy.get('ds-search-form input[name="query"]').type(queryString);
|
||||||
|
cy.get('ds-search-form button.search-button').click();
|
||||||
|
cy.url().should('include', '/search');
|
||||||
|
cy.url().should('include', 'query=' + encodeURI(queryString));
|
||||||
|
});
|
||||||
|
|
||||||
|
// it('should pass accessibility tests', () => {
|
||||||
|
// // first must inject Axe into current page
|
||||||
|
// cy.injectAxe();
|
||||||
|
|
||||||
|
// // Analyze entire page for accessibility issues
|
||||||
|
// // NOTE: this test checks accessibility of header/footer as well
|
||||||
|
// cy.checkA11y({
|
||||||
|
// exclude: [
|
||||||
|
// ['#klaro'], // Klaro plugin (privacy policy popup) has color contrast issues
|
||||||
|
// ['#search-navbar-container'], // search in navbar has duplicative ID. Will be fixed in #1174
|
||||||
|
// ['.dropdownLogin'] // "Log in" link in header has color contrast issues
|
||||||
|
// ],
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
});
|
15
cypress/integration/item-page.spec.ts
Normal file
15
cypress/integration/item-page.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
describe('Item Page', () => {
|
||||||
|
const ITEMPAGE = '/items/e98b0f27-5c19-49a0-960d-eb6ad5287067';
|
||||||
|
const ENTITYPAGE = '/entities/publication/e98b0f27-5c19-49a0-960d-eb6ad5287067';
|
||||||
|
|
||||||
|
it('should contain element ds-item-page when navigating to an item page', () => {
|
||||||
|
cy.visit(ENTITYPAGE);
|
||||||
|
cy.get('ds-item-page').should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid]
|
||||||
|
it('should redirect to the entity page when navigating to an item page', () => {
|
||||||
|
cy.visit(ITEMPAGE);
|
||||||
|
cy.location('pathname').should('eq', ENTITYPAGE);
|
||||||
|
});
|
||||||
|
});
|
25
cypress/integration/item-statistics.spec.ts
Normal file
25
cypress/integration/item-statistics.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
describe('Item Statistics Page', () => {
|
||||||
|
const ITEMUUID = 'e98b0f27-5c19-49a0-960d-eb6ad5287067';
|
||||||
|
const ITEMSTATISTICSPAGE = '/statistics/items/' + ITEMUUID;
|
||||||
|
|
||||||
|
it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => {
|
||||||
|
cy.visit(ITEMSTATISTICSPAGE);
|
||||||
|
cy.get('ds-item-statistics-page').should('exist');
|
||||||
|
cy.get('ds-item-page').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain the item statistics page url when navigating to an item statistics page', () => {
|
||||||
|
cy.visit(ITEMSTATISTICSPAGE);
|
||||||
|
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a "Total visits" section', () => {
|
||||||
|
cy.visit(ITEMSTATISTICSPAGE);
|
||||||
|
cy.get('.' + ITEMUUID + '_TotalVisits').should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a "Total visits per month" section', () => {
|
||||||
|
cy.visit(ITEMSTATISTICSPAGE);
|
||||||
|
cy.get('.' + ITEMUUID + '_TotalVisitsPerMonth').should('exist');
|
||||||
|
});
|
||||||
|
});
|
13
cypress/integration/pagenotfound.spec.ts
Normal file
13
cypress/integration/pagenotfound.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
describe('PageNotFound', () => {
|
||||||
|
it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => {
|
||||||
|
// request an invalid page (UUIDs at root path aren't valid)
|
||||||
|
cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false });
|
||||||
|
cy.get('ds-pagenotfound').should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not contain element ds-pagenotfound when navigating to existing page', () => {
|
||||||
|
cy.visit('/home');
|
||||||
|
cy.get('ds-pagenotfound').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
49
cypress/integration/search-navbar.spec.ts
Normal file
49
cypress/integration/search-navbar.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
const page = {
|
||||||
|
fillOutQueryInNavBar(query) {
|
||||||
|
// Click the magnifying glass
|
||||||
|
cy.get('.navbar-container #search-navbar-container form a').click();
|
||||||
|
// Fill out a query in input that appears
|
||||||
|
cy.get('.navbar-container #search-navbar-container form input[name = "query"]').type(query);
|
||||||
|
},
|
||||||
|
submitQueryByPressingEnter() {
|
||||||
|
cy.get('.navbar-container #search-navbar-container form input[name = "query"]').type('{enter}');
|
||||||
|
},
|
||||||
|
submitQueryByPressingIcon() {
|
||||||
|
cy.get('.navbar-container #search-navbar-container form .submit-icon').click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Search from Navigation Bar', () => {
|
||||||
|
// NOTE: these tests currently assume this query will return results!
|
||||||
|
const query = 'test';
|
||||||
|
|
||||||
|
it('should go to search page with correct query if submitted (from home)', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
page.fillOutQueryInNavBar(query);
|
||||||
|
page.submitQueryByPressingEnter();
|
||||||
|
// New URL should include query param
|
||||||
|
cy.url().should('include', 'query=' + query);
|
||||||
|
// At least one search result should be displayed
|
||||||
|
cy.get('ds-item-search-result-list-element').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should go to search page with correct query if submitted (from search)', () => {
|
||||||
|
cy.visit('/search');
|
||||||
|
page.fillOutQueryInNavBar(query);
|
||||||
|
page.submitQueryByPressingEnter();
|
||||||
|
// New URL should include query param
|
||||||
|
cy.url().should('include', 'query=' + query);
|
||||||
|
// At least one search result should be displayed
|
||||||
|
cy.get('ds-item-search-result-list-element').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow user to also submit query by clicking icon', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
page.fillOutQueryInNavBar(query);
|
||||||
|
page.submitQueryByPressingIcon();
|
||||||
|
// New URL should include query param
|
||||||
|
cy.url().should('include', 'query=' + query);
|
||||||
|
// At least one search result should be displayed
|
||||||
|
cy.get('ds-item-search-result-list-element').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
66
cypress/integration/search-page.spec.ts
Normal file
66
cypress/integration/search-page.spec.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
describe('Search Page', () => {
|
||||||
|
// unique ID of the search form (for selecting specific elements below)
|
||||||
|
const SEARCHFORM_ID = '#search-form';
|
||||||
|
|
||||||
|
it('should contain query value when navigating to page with query parameter', () => {
|
||||||
|
const queryString = 'test query';
|
||||||
|
cy.visit('/search?query=' + queryString);
|
||||||
|
cy.get(SEARCHFORM_ID + ' input[name="query"]').should('have.value', queryString);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should have right scope selected when navigating to page with scope parameter', () => {
|
||||||
|
// First, visit search with no params just to get the set of the scope options
|
||||||
|
cy.visit('/search');
|
||||||
|
cy.get(SEARCHFORM_ID + ' select[name="scope"] > option').as('options');
|
||||||
|
|
||||||
|
// Find length of scope options, select a random index
|
||||||
|
cy.get('@options').its('length')
|
||||||
|
.then(len => Math.floor(Math.random() * Math.floor(len)))
|
||||||
|
.then((index) => {
|
||||||
|
// return the option at that (randomly selected) index
|
||||||
|
return cy.get('@options').eq(index);
|
||||||
|
})
|
||||||
|
.then((option) => {
|
||||||
|
const randomScope: any = option.val();
|
||||||
|
// Visit the search page with the randomly selected option as a pararmeter
|
||||||
|
cy.visit('/search?scope=' + randomScope);
|
||||||
|
// Verify that scope is selected when the page reloads
|
||||||
|
cy.get(SEARCHFORM_ID + ' select[name="scope"]').find('option:selected').should('have.value', randomScope);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should redirect to the correct url when scope was set and submit button was triggered', () => {
|
||||||
|
// First, visit search with no params just to get the set of scope options
|
||||||
|
cy.visit('/search');
|
||||||
|
cy.get(SEARCHFORM_ID + ' select[name="scope"] > option').as('options');
|
||||||
|
|
||||||
|
// Find length of scope options, select a random index (i.e. a random option in selectbox)
|
||||||
|
cy.get('@options').its('length')
|
||||||
|
.then(len => Math.floor(Math.random() * Math.floor(len)))
|
||||||
|
.then((index) => {
|
||||||
|
// return the option at that (randomly selected) index
|
||||||
|
return cy.get('@options').eq(index);
|
||||||
|
})
|
||||||
|
.then((option) => {
|
||||||
|
const randomScope: any = option.val();
|
||||||
|
// Select the option at our random index & click the search button
|
||||||
|
cy.get(SEARCHFORM_ID + ' select[name="scope"]').select(randomScope);
|
||||||
|
cy.get(SEARCHFORM_ID + ' button.search-button').click();
|
||||||
|
// Result should be the page URL should include that scope & page will reload with scope selected
|
||||||
|
cy.url().should('include', 'scope=' + randomScope);
|
||||||
|
cy.get(SEARCHFORM_ID + ' select[name="scope"]').find('option:selected').should('have.value', randomScope);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect to the correct url when query was set and submit button was triggered', () => {
|
||||||
|
const queryString = 'Another interesting query string';
|
||||||
|
cy.visit('/search');
|
||||||
|
// Type query in searchbox & click search button
|
||||||
|
cy.get(SEARCHFORM_ID + ' input[name="query"]').type(queryString);
|
||||||
|
cy.get(SEARCHFORM_ID + ' button.search-button').click();
|
||||||
|
cy.url().should('include', 'query=' + encodeURI(queryString));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
5
cypress/plugins/index.ts
Normal file
5
cypress/plugins/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
|
||||||
|
// For more info, visit https://on.cypress.io/plugins-api
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
module.exports = (on, config) => { };
|
||||||
|
/* tslint:enable:no-empty */
|
43
cypress/support/commands.ts
Normal file
43
cypress/support/commands.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// ***********************************************
|
||||||
|
// This example namespace declaration will help
|
||||||
|
// with Intellisense and code completion in your
|
||||||
|
// IDE or Text Editor.
|
||||||
|
// ***********************************************
|
||||||
|
// declare namespace Cypress {
|
||||||
|
// interface Chainable<Subject = any> {
|
||||||
|
// customCommand(param: any): typeof customCommand;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// function customCommand(param: any): void {
|
||||||
|
// console.warn(param);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// NOTE: You can use it like so:
|
||||||
|
// Cypress.Commands.add('customCommand', customCommand);
|
||||||
|
//
|
||||||
|
// ***********************************************
|
||||||
|
// This example commands.js shows you how to
|
||||||
|
// create various custom commands and overwrite
|
||||||
|
// existing commands.
|
||||||
|
//
|
||||||
|
// For more comprehensive examples of custom
|
||||||
|
// commands please read more here:
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
// ***********************************************
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a parent command --
|
||||||
|
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a child command --
|
||||||
|
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a dual command --
|
||||||
|
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This will overwrite an existing command --
|
||||||
|
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
21
cypress/support/index.ts
Normal file
21
cypress/support/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// ***********************************************************
|
||||||
|
// This example support/index.js is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// When a command from ./commands is ready to use, import with `import './commands'` syntax
|
||||||
|
// import './commands';
|
||||||
|
|
||||||
|
// Import Cypress Axe tools for all tests
|
||||||
|
// https://github.com/component-driven/cypress-axe
|
||||||
|
import 'cypress-axe';
|
12
cypress/tsconfig.json
Normal file
12
cypress/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"**/*.ts"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": [
|
||||||
|
"cypress",
|
||||||
|
"cypress-axe"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,9 @@
|
|||||||
# Docker Compose files
|
# Docker Compose files
|
||||||
|
|
||||||
|
***
|
||||||
|
:warning: **NOT PRODUCTION READY** The below Docker Compose resources are not guaranteed "production ready" at this time. They have been built for development/testing only. Therefore, DSpace Docker images may not be fully secured or up-to-date. While you are welcome to base your own images on these DSpace images/resources, these should not be used "as is" in any production scenario.
|
||||||
|
***
|
||||||
|
|
||||||
## docker directory
|
## docker directory
|
||||||
- docker-compose.yml
|
- docker-compose.yml
|
||||||
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.
|
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.
|
||||||
|
@@ -1,14 +0,0 @@
|
|||||||
const config = require('./protractor.conf').config;
|
|
||||||
|
|
||||||
config.capabilities = {
|
|
||||||
browserName: 'chrome',
|
|
||||||
chromeOptions: {
|
|
||||||
args: ['--headless', '--no-sandbox', '--disable-gpu']
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// don't use protractor's webdriver, as it may be incompatible with the installed chrome version
|
|
||||||
config.directConnect = false;
|
|
||||||
config.seleniumAddress = 'http://localhost:4444/wd/hub';
|
|
||||||
|
|
||||||
exports.config = config;
|
|
@@ -1,92 +0,0 @@
|
|||||||
// Protractor configuration file, see link for more information
|
|
||||||
// https://github.com/angular/protractor/blob/master/docs/referenceConf.js
|
|
||||||
|
|
||||||
/*global jasmine */
|
|
||||||
var SpecReporter = require('jasmine-spec-reporter').SpecReporter;
|
|
||||||
|
|
||||||
exports.config = {
|
|
||||||
allScriptsTimeout: 600000,
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
// Uncomment to run tests using a remote Selenium server
|
|
||||||
//seleniumAddress: 'http://selenium.address:4444/wd/hub',
|
|
||||||
// Change to 'false' to run tests using a remote Selenium server
|
|
||||||
directConnect: true,
|
|
||||||
// Change if the website to test is not on the localhost
|
|
||||||
baseUrl: 'http://localhost:4000/',
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
specs: [
|
|
||||||
'./src/**/*.e2e-spec.ts'
|
|
||||||
],
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
// Browser and Capabilities: PhantomJS
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
// capabilities: {
|
|
||||||
// 'browserName': 'phantomjs',
|
|
||||||
// 'version': '',
|
|
||||||
// 'platform': 'ANY'
|
|
||||||
// },
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
// Browser and Capabilities: Chrome
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
capabilities: {
|
|
||||||
'browserName': 'chrome',
|
|
||||||
'version': '',
|
|
||||||
'platform': 'ANY',
|
|
||||||
'chromeOptions': {
|
|
||||||
'args': [ '--headless', '--disable-gpu' ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
// Browser and Capabilities: Firefox
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
// capabilities: {
|
|
||||||
// 'browserName': 'firefox',
|
|
||||||
// 'version': '',
|
|
||||||
// 'platform': 'ANY'
|
|
||||||
// },
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
// Browser and Capabilities: MultiCapabilities
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
//multiCapabilities: [
|
|
||||||
// {
|
|
||||||
// 'browserName': 'phantomjs',
|
|
||||||
// 'version': '',
|
|
||||||
// 'platform': 'ANY'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// 'browserName': 'chrome',
|
|
||||||
// 'version': '',
|
|
||||||
// 'platform': 'ANY'
|
|
||||||
// }
|
|
||||||
// {
|
|
||||||
// 'browserName': 'firefox',
|
|
||||||
// 'version': '',
|
|
||||||
// 'platform': 'ANY'
|
|
||||||
// }
|
|
||||||
//],
|
|
||||||
|
|
||||||
plugins: [{
|
|
||||||
path: '../node_modules/protractor-istanbul-plugin'
|
|
||||||
}],
|
|
||||||
|
|
||||||
framework: 'jasmine',
|
|
||||||
jasmineNodeOpts: {
|
|
||||||
showColors: true,
|
|
||||||
defaultTimeoutInterval: 600000,
|
|
||||||
print: function () {}
|
|
||||||
},
|
|
||||||
useAllAngular2AppRoots: true,
|
|
||||||
beforeLaunch: function () {
|
|
||||||
require('ts-node').register({
|
|
||||||
project: './e2e/tsconfig.json'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onPrepare: function () {
|
|
||||||
jasmine.getEnv().addReporter(new SpecReporter({
|
|
||||||
spec: {
|
|
||||||
displayStacktrace: true
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,22 +0,0 @@
|
|||||||
import { ProtractorPage } from './app.po';
|
|
||||||
|
|
||||||
describe('protractor App', () => {
|
|
||||||
let page: ProtractorPage;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
page = new ProtractorPage();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display translated title "DSpace Angular :: Home"', () => {
|
|
||||||
page.navigateTo();
|
|
||||||
page.waitUntilNotLoading();
|
|
||||||
expect<any>(page.getPageTitleText()).toEqual('DSpace Angular :: Home');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain a news section', () => {
|
|
||||||
page.navigateTo();
|
|
||||||
page.waitUntilNotLoading();
|
|
||||||
const text = page.getHomePageNewsText();
|
|
||||||
expect<any>(text).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,23 +0,0 @@
|
|||||||
import { browser, element, by, protractor, promise } from 'protractor';
|
|
||||||
|
|
||||||
export class ProtractorPage {
|
|
||||||
navigateTo() {
|
|
||||||
return browser.get('/')
|
|
||||||
.then(() => browser.waitForAngular());
|
|
||||||
}
|
|
||||||
|
|
||||||
getPageTitleText() {
|
|
||||||
return browser.getTitle();
|
|
||||||
}
|
|
||||||
|
|
||||||
getHomePageNewsText() {
|
|
||||||
return element(by.css('ds-home-news')).getText();
|
|
||||||
}
|
|
||||||
|
|
||||||
waitUntilNotLoading(): promise.Promise<unknown> {
|
|
||||||
const loading = element(by.css('.loader'));
|
|
||||||
const EC = protractor.ExpectedConditions;
|
|
||||||
const notLoading = EC.not(EC.presenceOf(loading));
|
|
||||||
return browser.wait(notLoading, 10000);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,19 +0,0 @@
|
|||||||
import { ProtractorPage } from './pagenotfound.po';
|
|
||||||
|
|
||||||
describe('protractor PageNotFound', () => {
|
|
||||||
let page: ProtractorPage;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
page = new ProtractorPage();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => {
|
|
||||||
page.navigateToNonExistingPage();
|
|
||||||
expect<any>(page.elementTagExists('ds-pagenotfound')).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not contain element ds-pagenotfound when navigating to existing page', () => {
|
|
||||||
page.navigateToExistingPage();
|
|
||||||
expect<any>(page.elementTagExists('ds-pagenotfound')).toEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,18 +0,0 @@
|
|||||||
import { browser, element, by } from 'protractor';
|
|
||||||
|
|
||||||
export class ProtractorPage {
|
|
||||||
HOMEPAGE = '/home';
|
|
||||||
NONEXISTINGPAGE = '/e9019a69-d4f1-4773-b6a3-bd362caa46f2';
|
|
||||||
|
|
||||||
navigateToNonExistingPage() {
|
|
||||||
return browser.get(this.NONEXISTINGPAGE);
|
|
||||||
}
|
|
||||||
navigateToExistingPage() {
|
|
||||||
return browser.get(this.HOMEPAGE);
|
|
||||||
}
|
|
||||||
|
|
||||||
elementTagExists(tag: string) {
|
|
||||||
return element(by.tagName(tag)).isPresent();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,46 +0,0 @@
|
|||||||
import { ProtractorPage } from './search-navbar.po';
|
|
||||||
import { browser } from 'protractor';
|
|
||||||
|
|
||||||
describe('protractor SearchNavbar', () => {
|
|
||||||
let page: ProtractorPage;
|
|
||||||
let queryString: string;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
page = new ProtractorPage();
|
|
||||||
queryString = 'the test query';
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should go to search page with correct query if submitted (from home)', () => {
|
|
||||||
page.navigateToHome();
|
|
||||||
return checkIfSearchWorks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should go to search page with correct query if submitted (from search)', () => {
|
|
||||||
page.navigateToSearch();
|
|
||||||
return checkIfSearchWorks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('check if can submit search box with pressing button', () => {
|
|
||||||
page.navigateToHome();
|
|
||||||
page.expandAndFocusSearchBox();
|
|
||||||
page.setCurrentQuery(queryString);
|
|
||||||
page.submitNavbarSearchForm();
|
|
||||||
browser.wait(() => {
|
|
||||||
return browser.getCurrentUrl().then((url: string) => {
|
|
||||||
return url.indexOf('query=' + encodeURI(queryString)) !== -1;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function checkIfSearchWorks(): boolean {
|
|
||||||
page.setCurrentQuery(queryString);
|
|
||||||
page.submitByPressingEnter();
|
|
||||||
browser.wait(() => {
|
|
||||||
return browser.getCurrentUrl().then((url: string) => {
|
|
||||||
return url.indexOf('query=' + encodeURI(queryString)) !== -1;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
@@ -1,35 +0,0 @@
|
|||||||
import { browser, by, element, protractor } from 'protractor';
|
|
||||||
import { promise } from 'selenium-webdriver';
|
|
||||||
|
|
||||||
export class ProtractorPage {
|
|
||||||
HOME = '/home';
|
|
||||||
SEARCH = '/search';
|
|
||||||
|
|
||||||
navigateToHome() {
|
|
||||||
return browser.get(this.HOME);
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToSearch() {
|
|
||||||
return browser.get(this.SEARCH);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentQuery(): promise.Promise<string> {
|
|
||||||
return element(by.css('.navbar-container #search-navbar-container form input')).getAttribute('value');
|
|
||||||
}
|
|
||||||
|
|
||||||
expandAndFocusSearchBox() {
|
|
||||||
element(by.css('.navbar-container #search-navbar-container form a')).click();
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentQuery(query: string) {
|
|
||||||
element(by.css('.navbar-container #search-navbar-container form input[name="query"]')).sendKeys(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
submitNavbarSearchForm() {
|
|
||||||
element(by.css('.navbar-container #search-navbar-container form .submit-icon')).click();
|
|
||||||
}
|
|
||||||
|
|
||||||
submitByPressingEnter() {
|
|
||||||
element(by.css('.navbar-container #search-navbar-container form input[name="query"]')).sendKeys(protractor.Key.ENTER);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,60 +0,0 @@
|
|||||||
import { ProtractorPage } from './search-page.po';
|
|
||||||
import { browser } from 'protractor';
|
|
||||||
|
|
||||||
describe('protractor SearchPage', () => {
|
|
||||||
let page: ProtractorPage;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
page = new ProtractorPage();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain query value when navigating to page with query parameter', () => {
|
|
||||||
const queryString = 'Interesting query string';
|
|
||||||
page.navigateToSearchWithQueryParameter(queryString)
|
|
||||||
.then(() => page.getCurrentQuery())
|
|
||||||
.then((query: string) => {
|
|
||||||
expect<string>(query).toEqual(queryString);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have right scope selected when navigating to page with scope parameter', () => {
|
|
||||||
page.navigateToSearch()
|
|
||||||
.then(() => page.getRandomScopeOption())
|
|
||||||
.then((scopeString: string) => {
|
|
||||||
page.navigateToSearchWithScopeParameter(scopeString);
|
|
||||||
page.waitUntilNotLoading();
|
|
||||||
page.getCurrentScope()
|
|
||||||
.then((s: string) => {
|
|
||||||
expect<string>(s).toEqual(scopeString);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect to the correct url when scope was set and submit button was triggered', () => {
|
|
||||||
page.navigateToSearch()
|
|
||||||
.then(() => page.getRandomScopeOption())
|
|
||||||
.then((scopeString: string) => {
|
|
||||||
page.setCurrentScope(scopeString)
|
|
||||||
.then(() => page.submitSearchForm())
|
|
||||||
.then(() => page.waitUntilNotLoading())
|
|
||||||
.then(() => () => {
|
|
||||||
browser.wait(() => {
|
|
||||||
return browser.getCurrentUrl().then((url: string) => {
|
|
||||||
return url.indexOf('scope=' + encodeURI(scopeString)) !== -1;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect to the correct url when query was set and submit button was triggered', () => {
|
|
||||||
const queryString = 'Another interesting query string';
|
|
||||||
page.setCurrentQuery(queryString);
|
|
||||||
page.submitSearchForm();
|
|
||||||
browser.wait(() => {
|
|
||||||
return browser.getCurrentUrl().then((url: string) => {
|
|
||||||
return url.indexOf('query=' + encodeURI(queryString)) !== -1;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,55 +0,0 @@
|
|||||||
import { browser, by, element, protractor } from 'protractor';
|
|
||||||
import { promise } from 'selenium-webdriver';
|
|
||||||
|
|
||||||
export class ProtractorPage {
|
|
||||||
SEARCH = '/search';
|
|
||||||
|
|
||||||
navigateToSearch() {
|
|
||||||
return browser.get(this.SEARCH);
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToSearchWithQueryParameter(query: string) {
|
|
||||||
return browser.get(this.SEARCH + '?query=' + query);
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToSearchWithScopeParameter(scope: string) {
|
|
||||||
return browser.get(this.SEARCH + '?scope=' + scope);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentScope(): promise.Promise<string> {
|
|
||||||
const scopeSelect = element(by.css('#search-form select'));
|
|
||||||
browser.wait(protractor.ExpectedConditions.presenceOf(scopeSelect), 10000);
|
|
||||||
return scopeSelect.getAttribute('value');
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentQuery(): promise.Promise<string> {
|
|
||||||
return element(by.css('#search-form input')).getAttribute('value');
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentScope(scope: string) {
|
|
||||||
return element(by.css('#search-form option[value="' + scope + '"]')).click();
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentQuery(query: string) {
|
|
||||||
element(by.css('#search-form input[name="query"]')).sendKeys(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
submitSearchForm() {
|
|
||||||
return element(by.css('#search-form button.search-button')).click();
|
|
||||||
}
|
|
||||||
|
|
||||||
getRandomScopeOption(): promise.Promise<string> {
|
|
||||||
const options = element(by.css('select[name="scope"]')).all(by.tagName('option'));
|
|
||||||
return options.count().then((c: number) => {
|
|
||||||
const index: number = Math.floor(Math.random() * (c - 1));
|
|
||||||
return options.get(index + 1).getAttribute('value');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
waitUntilNotLoading(): promise.Promise<unknown> {
|
|
||||||
const loading = element(by.css('.loader'));
|
|
||||||
const EC = protractor.ExpectedConditions;
|
|
||||||
const notLoading = EC.not(EC.presenceOf(loading));
|
|
||||||
return browser.wait(notLoading, 10000);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"compileOnSave": false,
|
|
||||||
"compilerOptions": {
|
|
||||||
"declaration": false,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"module": "commonjs",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"outDir": "../dist/out-tsc-e2e",
|
|
||||||
"sourceMap": true,
|
|
||||||
"target": "es2018",
|
|
||||||
"typeRoots": [
|
|
||||||
"../node_modules/@types"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
23
package.json
23
package.json
@@ -15,11 +15,11 @@
|
|||||||
"pretest:headless": "yarn run config:test",
|
"pretest:headless": "yarn run config:test",
|
||||||
"prebuild:prod": "yarn run config:prod",
|
"prebuild:prod": "yarn run config:prod",
|
||||||
"pree2e": "yarn run config:prod",
|
"pree2e": "yarn run config:prod",
|
||||||
"pree2e:ci": "yarn run config:prod",
|
|
||||||
"start": "yarn run start:prod",
|
"start": "yarn run start:prod",
|
||||||
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
|
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
|
||||||
"start:dev": "npm-run-all --parallel config:dev:watch serve",
|
"start:dev": "npm-run-all --parallel config:dev:watch serve",
|
||||||
"start:prod": "yarn run build:prod && yarn run serve:ssr",
|
"start:prod": "yarn run build:prod && yarn run serve:ssr",
|
||||||
|
"start:mirador:prod": "yarn run build:mirador && yarn run start:prod",
|
||||||
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
|
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"build:stats": "ng build --stats-json",
|
"build:stats": "ng build --stats-json",
|
||||||
@@ -32,7 +32,6 @@
|
|||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"lint-fix": "ng lint --fix=true",
|
"lint-fix": "ng lint --fix=true",
|
||||||
"e2e": "ng e2e",
|
"e2e": "ng e2e",
|
||||||
"e2e:ci": "ng e2e --webdriver-update=false --protractor-config=./e2e/protractor-ci.conf.js",
|
|
||||||
"compile:server": "webpack --config webpack.server.config.js --progress --color",
|
"compile:server": "webpack --config webpack.server.config.js --progress --color",
|
||||||
"serve:ssr": "node dist/server",
|
"serve:ssr": "node dist/server",
|
||||||
"clean:coverage": "rimraf coverage",
|
"clean:coverage": "rimraf coverage",
|
||||||
@@ -46,12 +45,11 @@
|
|||||||
"clean": "yarn run clean:prod && yarn run clean:env && yarn run clean:node",
|
"clean": "yarn run clean:prod && yarn run clean:env && yarn run clean:node",
|
||||||
"clean:env": "rimraf src/environments/environment.ts",
|
"clean:env": "rimraf src/environments/environment.ts",
|
||||||
"sync-i18n": "yarn run config:dev && ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts",
|
"sync-i18n": "yarn run config:dev && ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts",
|
||||||
"postinstall": "ngcc",
|
|
||||||
"build:mirador": "webpack --config webpack/webpack.mirador.config.ts",
|
"build:mirador": "webpack --config webpack/webpack.mirador.config.ts",
|
||||||
"build:client-and-server-bundles:mirador": "node --max-old-space-size=4096 ./node_modules/@angular/cli/bin/ng build --prod && ./node_modules/@angular/cli/bin/ng run dspace-angular:server:production --bundleDependencies true",
|
"merge-i18n": "yarn run config:dev && ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts",
|
||||||
"build:ssr:mirador": "yarn run build:client-and-server-bundles:mirador && yarn run compile:server",
|
"postinstall": "ngcc",
|
||||||
"start:prod:mirador": "yarn run build:ssr:mirador && yarn run serve:ssr",
|
"cypress:open": "cypress open",
|
||||||
"start:mirador:prod": "yarn run build:mirador && yarn run start:prod:mirador"
|
"cypress:run": "cypress run"
|
||||||
},
|
},
|
||||||
"browser": {
|
"browser": {
|
||||||
"fs": false,
|
"fs": false,
|
||||||
@@ -61,7 +59,8 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"minimist": "^1.2.5"
|
"minimist": "^1.2.5",
|
||||||
|
"webdriver-manager": "^12.1.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "~10.2.3",
|
"@angular/animations": "~10.2.3",
|
||||||
@@ -76,6 +75,7 @@
|
|||||||
"@angular/platform-server": "~10.2.3",
|
"@angular/platform-server": "~10.2.3",
|
||||||
"@angular/router": "~10.2.3",
|
"@angular/router": "~10.2.3",
|
||||||
"@angularclass/bootloader": "1.0.1",
|
"@angularclass/bootloader": "1.0.1",
|
||||||
|
"@kolkov/ngx-gallery": "^1.2.3",
|
||||||
"@ng-bootstrap/ng-bootstrap": "7.0.0",
|
"@ng-bootstrap/ng-bootstrap": "7.0.0",
|
||||||
"@ng-dynamic-forms/core": "^12.0.0",
|
"@ng-dynamic-forms/core": "^12.0.0",
|
||||||
"@ng-dynamic-forms/ui-ng-bootstrap": "^12.0.0",
|
"@ng-dynamic-forms/ui-ng-bootstrap": "^12.0.0",
|
||||||
@@ -132,8 +132,7 @@
|
|||||||
"sortablejs": "1.13.0",
|
"sortablejs": "1.13.0",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"webfontloader": "1.6.28",
|
"webfontloader": "1.6.28",
|
||||||
"zone.js": "^0.10.3",
|
"zone.js": "^0.10.3"
|
||||||
"@kolkov/ngx-gallery": "^1.2.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "10.0.1",
|
"@angular-builders/custom-webpack": "10.0.1",
|
||||||
@@ -141,6 +140,7 @@
|
|||||||
"@angular/cli": "~10.2.0",
|
"@angular/cli": "~10.2.0",
|
||||||
"@angular/compiler-cli": "~10.2.3",
|
"@angular/compiler-cli": "~10.2.3",
|
||||||
"@angular/language-service": "~10.2.3",
|
"@angular/language-service": "~10.2.3",
|
||||||
|
"@cypress/schematic": "^1.5.0",
|
||||||
"@fortawesome/fontawesome-free": "^5.5.0",
|
"@fortawesome/fontawesome-free": "^5.5.0",
|
||||||
"@ngrx/store-devtools": "^10.0.1",
|
"@ngrx/store-devtools": "^10.0.1",
|
||||||
"@ngtools/webpack": "10.2.0",
|
"@ngtools/webpack": "10.2.0",
|
||||||
@@ -153,11 +153,14 @@
|
|||||||
"@types/js-cookie": "2.2.6",
|
"@types/js-cookie": "2.2.6",
|
||||||
"@types/lodash": "^4.14.165",
|
"@types/lodash": "^4.14.165",
|
||||||
"@types/node": "^14.14.9",
|
"@types/node": "^14.14.9",
|
||||||
|
"axe-core": "^4.3.3",
|
||||||
"codelyzer": "^6.0.1",
|
"codelyzer": "^6.0.1",
|
||||||
"compression-webpack-plugin": "^3.0.1",
|
"compression-webpack-plugin": "^3.0.1",
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
"css-loader": "3.4.0",
|
"css-loader": "3.4.0",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^4.1.10",
|
||||||
|
"cypress": "8.3.1",
|
||||||
|
"cypress-axe": "^0.13.0",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"fork-ts-checker-webpack-plugin": "^6.0.3",
|
"fork-ts-checker-webpack-plugin": "^6.0.3",
|
||||||
|
99
scripts/merge-i18n-files.ts
Normal file
99
scripts/merge-i18n-files.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { projectRoot} from '../webpack/helpers';
|
||||||
|
const commander = require('commander');
|
||||||
|
const fs = require('fs');
|
||||||
|
const JSON5 = require('json5');
|
||||||
|
const _cliProgress = require('cli-progress');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const program = new commander.Command();
|
||||||
|
program.version('1.0.0', '-v, --version');
|
||||||
|
|
||||||
|
const LANGUAGE_FILES_LOCATION = 'src/assets/i18n';
|
||||||
|
|
||||||
|
parseCliInput();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purpose: Allows customization of i18n labels from within themes
|
||||||
|
* e.g. Customize the label "menu.section.browse_global" to display "Browse DSpace" rather than "All of DSpace"
|
||||||
|
*
|
||||||
|
* This script uses the i18n files found in a source directory to override settings in files with the same
|
||||||
|
* name in a destination directory. Only the i18n labels to be overridden need be in the source files.
|
||||||
|
*
|
||||||
|
* Execution (using custom theme):
|
||||||
|
* ```
|
||||||
|
* yarn merge-i18n -s src/themes/custom/assets/i18n
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Input parameters:
|
||||||
|
* * Output directory: The directory in which the original i18n files are stored
|
||||||
|
* - Defaults to src/assets/i18n (the default i18n file location)
|
||||||
|
* - This is where the final output files will be written
|
||||||
|
* * Source directory: The directory with override files
|
||||||
|
* - Required
|
||||||
|
* - Recommended to place override files in the theme directory under assets/i18n (but this is not required)
|
||||||
|
* - Files must have matching names in both source and destination directories, for example:
|
||||||
|
* en.json5 in the source directory will be merged with en.json5 in the destination directory
|
||||||
|
* fr.json5 in the source directory will be merged with fr.json5 in the destination directory
|
||||||
|
*/
|
||||||
|
function parseCliInput() {
|
||||||
|
program
|
||||||
|
.option('-d, --output-dir <output-dir>', 'output dir when running script on all language files', projectRoot(LANGUAGE_FILES_LOCATION))
|
||||||
|
.option('-s, --source-dir <source-dir>', 'source dir of transalations to be merged')
|
||||||
|
.usage('(-s <source-dir> [-d <output-dir>])')
|
||||||
|
.parse(process.argv);
|
||||||
|
|
||||||
|
if (program.outputDir && program.sourceDir) {
|
||||||
|
if (!fs.existsSync(program.outputDir) && !fs.lstatSync(program.outputDir).isDirectory() ) {
|
||||||
|
console.error('Output does not exist or is not a directory.');
|
||||||
|
console.log(program.outputHelp());
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(program.sourceDir) && !fs.lstatSync(program.sourceDir).isDirectory() ) {
|
||||||
|
console.error('Source does not exist or is not a directory.');
|
||||||
|
console.log(program.outputHelp());
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
fs.readdirSync(projectRoot(program.sourceDir)).forEach(file => {
|
||||||
|
if (fs.existsSync(program.outputDir + '/' + file) ) {
|
||||||
|
console.log('Merging: ' + program.outputDir + '/' + file + ' with ' + program.sourceDir + '/' + file);
|
||||||
|
mergeFileWithSource(program.sourceDir + '/' + file, program.outputDir + '/' + file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Source or Output parameter is missing.');
|
||||||
|
console.log(program.outputHelp());
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads source file and output file to merge the contents
|
||||||
|
* > Iterates over the source file keys
|
||||||
|
* > Updates values for each key and adds new keys as needed
|
||||||
|
* > Updates the output file with the new merged json
|
||||||
|
* @param pathToSourceFile Valid path to source file to merge from
|
||||||
|
* @param pathToOutputFile Valid path to merge and write output
|
||||||
|
*/
|
||||||
|
function mergeFileWithSource(pathToSourceFile, pathToOutputFile) {
|
||||||
|
const progressBar = new _cliProgress.SingleBar({}, _cliProgress.Presets.shades_classic);
|
||||||
|
progressBar.start(100, 0);
|
||||||
|
|
||||||
|
const sourceFile = fs.readFileSync(pathToSourceFile, 'utf8');
|
||||||
|
progressBar.update(10);
|
||||||
|
const outputFile = fs.readFileSync(pathToOutputFile, 'utf8');
|
||||||
|
progressBar.update(20);
|
||||||
|
|
||||||
|
const parsedSource = JSON5.parse(sourceFile);
|
||||||
|
progressBar.update(30);
|
||||||
|
const parsedOutput = JSON5.parse(outputFile);
|
||||||
|
progressBar.update(40);
|
||||||
|
|
||||||
|
for (const key of Object.keys(parsedSource)) {
|
||||||
|
parsedOutput[key] = parsedSource[key];
|
||||||
|
}
|
||||||
|
progressBar.update(80);
|
||||||
|
fs.writeFileSync(pathToOutputFile,JSON5.stringify(parsedOutput,{ space:'\n ', quote: '"' }), { encoding:'utf8' });
|
||||||
|
|
||||||
|
progressBar.update(100);
|
||||||
|
progressBar.stop();
|
||||||
|
}
|
@@ -1,11 +0,0 @@
|
|||||||
<li class="sidebar-section">
|
|
||||||
<a class="nav-item nav-link shortcut-icon" attr.aria-labelledby="sidebarName-{{section.id}}" [title]="('menu.section.icon.' + section.id) | translate" [routerLink]="itemModel.link">
|
|
||||||
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
|
||||||
</a>
|
|
||||||
<div class="sidebar-collapsible">
|
|
||||||
<span id="sidebarName-{{section.id}}" class="section-header-text">
|
|
||||||
<ng-container
|
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
@@ -1,27 +0,0 @@
|
|||||||
<li class="sidebar-section" [ngClass]="{'expanded': (expanded | async)}"
|
|
||||||
[@bgColor]="{
|
|
||||||
value: ((expanded | async) ? 'endBackground' : 'startBackground'),
|
|
||||||
params: {endColor: (sidebarActiveBg | async)}}">
|
|
||||||
<div class="icon-wrapper">
|
|
||||||
<a class="nav-item nav-link shortcut-icon" attr.aria.labelledby="sidebarName-{{section.id}}" [title]="('menu.section.icon.' + section.id) | translate" (click)="toggleSection($event)" href="#">
|
|
||||||
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="sidebar-collapsible">
|
|
||||||
<a class="nav-item nav-link" href="#"
|
|
||||||
(click)="toggleSection($event)">
|
|
||||||
<span id="sidebarName-{{section.id}}" class="section-header-text">
|
|
||||||
<ng-container
|
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
|
||||||
</span>
|
|
||||||
<i class="fas fa-chevron-right fa-pull-right"
|
|
||||||
[@rotate]="(expanded | async) ? 'expanded' : 'collapsed'" [title]="('menu.section.toggle.' + section.id) | translate"></i>
|
|
||||||
</a>
|
|
||||||
<ul class="sidebar-sub-level-items list-unstyled" @slide *ngIf="(expanded | async)">
|
|
||||||
<li *ngFor="let subSection of (subSections$ | async)">
|
|
||||||
<ng-container
|
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
@@ -1,38 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component';
|
|
||||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
|
||||||
import { BitstreamPageResolver } from './bitstream-page.resolver';
|
|
||||||
import { BitstreamDownloadPageComponent } from '../shared/bitstream-download-page/bitstream-download-page.component';
|
|
||||||
|
|
||||||
const EDIT_BITSTREAM_PATH = ':id/edit';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Routing module to help navigate Bitstream pages
|
|
||||||
*/
|
|
||||||
@NgModule({
|
|
||||||
imports: [
|
|
||||||
RouterModule.forChild([
|
|
||||||
{
|
|
||||||
path:':id/download',
|
|
||||||
component: BitstreamDownloadPageComponent,
|
|
||||||
resolve: {
|
|
||||||
bitstream: BitstreamPageResolver
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: EDIT_BITSTREAM_PATH,
|
|
||||||
component: EditBitstreamPageComponent,
|
|
||||||
resolve: {
|
|
||||||
bitstream: BitstreamPageResolver
|
|
||||||
},
|
|
||||||
canActivate: [AuthenticatedGuard]
|
|
||||||
}
|
|
||||||
])
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
BitstreamPageResolver,
|
|
||||||
]
|
|
||||||
})
|
|
||||||
export class BitstreamPageRoutingModule {
|
|
||||||
}
|
|
@@ -1,22 +0,0 @@
|
|||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
|
||||||
<div class="col-12 pb-4">
|
|
||||||
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate}}</h2>
|
|
||||||
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
|
||||||
<div class="form-group row">
|
|
||||||
<div class="col text-right">
|
|
||||||
<button class="btn btn-outline-secondary" (click)="onCancel(dso)">
|
|
||||||
<i class="fas fa-times"></i> {{'community.delete.cancel' | translate}}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)">
|
|
||||||
<i class="fas fa-trash"></i> {{'community.delete.confirm' | translate}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
@@ -1,48 +0,0 @@
|
|||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<h2>{{'item.edit.move.head' | translate: {id: (itemRD$ | async)?.payload?.handle} }}</h2>
|
|
||||||
<p>{{'item.edit.move.description' | translate}}</p>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<ds-dso-input-suggestions #f id="search-form"
|
|
||||||
[suggestions]="(collectionSearchResults | async)"
|
|
||||||
[placeholder]="'item.edit.move.search.placeholder'| translate"
|
|
||||||
[action]="getCurrentUrl()"
|
|
||||||
[name]="'item-move'"
|
|
||||||
[(ngModel)]="selectedCollectionName"
|
|
||||||
(clickSuggestion)="onClick($event)"
|
|
||||||
(typeSuggestion)="resetCollection($event)"
|
|
||||||
(findSuggestions)="findSuggestions($event)"
|
|
||||||
(click)="f.open()"
|
|
||||||
ngDefaultControl>
|
|
||||||
</ds-dso-input-suggestions>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<p>
|
|
||||||
<input type="checkbox" name="tc" [(ngModel)]="inheritPolicies" id="inheritPoliciesCheckbox">
|
|
||||||
<label for="inheritPoliciesCheckbox">{{'item.edit.move.inheritpolicies.checkbox' |
|
|
||||||
translate}}</label>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{'item.edit.move.inheritpolicies.description' | translate}}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button (click)="moveCollection()" class="btn btn-primary" [disabled]=!canSubmit>
|
|
||||||
<span *ngIf="!processing"> {{'item.edit.move.move' | translate}}</span>
|
|
||||||
<span *ngIf="processing"><i class='fas fa-circle-notch fa-spin'></i>
|
|
||||||
{{'item.edit.move.processing' | translate}}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button [routerLink]="[(itemPageRoute$ | async), 'edit']"
|
|
||||||
class="btn btn-outline-secondary">
|
|
||||||
{{'item.edit.move.cancel' | translate}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@@ -1,15 +0,0 @@
|
|||||||
<div class="col-3 float-left d-flex h-100 action-label">
|
|
||||||
<span class="justify-content-center align-self-center">
|
|
||||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.label' | translate}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="!operation.disabled" class="col-9 float-left action-button">
|
|
||||||
<a class="btn btn-outline-primary" [routerLink]="operation.operationUrl">
|
|
||||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="operation.disabled" class="col-9 float-left action-button">
|
|
||||||
<span class="btn btn-danger">
|
|
||||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
@@ -1,26 +0,0 @@
|
|||||||
<h5>
|
|
||||||
{{getRelationshipMessageKey() | async | translate}}
|
|
||||||
<button class="ml-2 btn btn-success" (click)="openLookup()">
|
|
||||||
<i class="fas fa-plus"></i>
|
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.relationships.edit.buttons.add" | translate}}</span>
|
|
||||||
</button>
|
|
||||||
</h5>
|
|
||||||
<ng-container *ngVar="updates$ | async as updates">
|
|
||||||
<ng-container *ngIf="updates">
|
|
||||||
<ng-container *ngVar="updates | dsObjectValues as updateValues">
|
|
||||||
<ds-edit-relationship *ngFor="let updateValue of updateValues; trackBy: trackUpdate"
|
|
||||||
class="relationship-row d-block alert"
|
|
||||||
[fieldUpdate]="updateValue || {}"
|
|
||||||
[url]="url"
|
|
||||||
[editItem]="item"
|
|
||||||
[ngClass]="{
|
|
||||||
'alert-success': updateValue.changeType === 1,
|
|
||||||
'alert-warning': updateValue.changeType === 0,
|
|
||||||
'alert-danger': updateValue.changeType === 2
|
|
||||||
}">
|
|
||||||
</ds-edit-relationship>
|
|
||||||
<div *ngIf="updateValues.length === 0">{{"item.edit.relationships.no-relationships" | translate}}</div>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
<ds-loading *ngIf="!updates"></ds-loading>
|
|
||||||
</ng-container>
|
|
@@ -1,95 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.component';
|
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-component-without-content',
|
|
||||||
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
|
||||||
'</ds-metadata-field-wrapper>'
|
|
||||||
})
|
|
||||||
class NoContentComponent {}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-component-with-empty-spans',
|
|
||||||
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
|
||||||
' <span></span>\n' +
|
|
||||||
' <span></span>\n' +
|
|
||||||
'</ds-metadata-field-wrapper>'
|
|
||||||
})
|
|
||||||
class SpanContentComponent {}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-component-with-text',
|
|
||||||
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
|
||||||
' <span>The quick brown fox jumps over the lazy dog</span>\n' +
|
|
||||||
'</ds-metadata-field-wrapper>'
|
|
||||||
})
|
|
||||||
class TextContentComponent {}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-component-with-image',
|
|
||||||
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
|
||||||
' <img src="https://some/image.png" alt="an alt text">\n' +
|
|
||||||
'</ds-metadata-field-wrapper>'
|
|
||||||
})
|
|
||||||
class ImgContentComponent {}
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
|
||||||
|
|
||||||
describe('MetadataFieldWrapperComponent', () => {
|
|
||||||
let component: MetadataFieldWrapperComponent;
|
|
||||||
let fixture: ComponentFixture<MetadataFieldWrapperComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
declarations: [MetadataFieldWrapperComponent, NoContentComponent, SpanContentComponent, TextContentComponent, ImgContentComponent]
|
|
||||||
}).compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(MetadataFieldWrapperComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
});
|
|
||||||
|
|
||||||
const wrapperSelector = '.simple-view-element';
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show the component when there is no content', () => {
|
|
||||||
const parentFixture = TestBed.createComponent(NoContentComponent);
|
|
||||||
parentFixture.detectChanges();
|
|
||||||
const parentNative = parentFixture.nativeElement;
|
|
||||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
|
||||||
expect(nativeWrapper.classList.contains('d-none')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show the component when there is DOM content but not text or an image', () => {
|
|
||||||
const parentFixture = TestBed.createComponent(SpanContentComponent);
|
|
||||||
parentFixture.detectChanges();
|
|
||||||
const parentNative = parentFixture.nativeElement;
|
|
||||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
|
||||||
expect(nativeWrapper.classList.contains('d-none')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show the component when there is text content', () => {
|
|
||||||
const parentFixture = TestBed.createComponent(TextContentComponent);
|
|
||||||
parentFixture.detectChanges();
|
|
||||||
const parentNative = parentFixture.nativeElement;
|
|
||||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
|
||||||
parentFixture.detectChanges();
|
|
||||||
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show the component when there is img content', () => {
|
|
||||||
const parentFixture = TestBed.createComponent(ImgContentComponent);
|
|
||||||
parentFixture.detectChanges();
|
|
||||||
const parentNative = parentFixture.nativeElement;
|
|
||||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
|
||||||
parentFixture.detectChanges();
|
|
||||||
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
@@ -1,39 +0,0 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { environment } from '../../../../../environments/environment';
|
|
||||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
|
||||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
|
||||||
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
|
|
||||||
import { getItemPageRoute } from '../../../item-page-routing-paths';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-item',
|
|
||||||
template: ''
|
|
||||||
})
|
|
||||||
/**
|
|
||||||
* A generic component for displaying metadata and relations of an item
|
|
||||||
*/
|
|
||||||
export class ItemComponent implements OnInit {
|
|
||||||
@Input() object: Item;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Route to the item page
|
|
||||||
*/
|
|
||||||
itemPageRoute: string;
|
|
||||||
mediaViewer = environment.mediaViewer;
|
|
||||||
|
|
||||||
constructor(protected bitstreamDataService: BitstreamDataService) {
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.itemPageRoute = getItemPageRoute(this.object);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO refactor to return RemoteData, and thumbnail template to deal with loading
|
|
||||||
getThumbnail(): Observable<Bitstream> {
|
|
||||||
return this.bitstreamDataService.getThumbnailFor(this.object).pipe(
|
|
||||||
getFirstSucceededRemoteDataPayload()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,112 +0,0 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
||||||
import { By } from '@angular/platform-browser';
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
|
||||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
|
||||||
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
|
||||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
|
||||||
import { CommunityDataService } from '../../../../core/data/community-data.service';
|
|
||||||
import { DefaultChangeAnalyzer } from '../../../../core/data/default-change-analyzer.service';
|
|
||||||
import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service';
|
|
||||||
import { ItemDataService } from '../../../../core/data/item-data.service';
|
|
||||||
import { buildPaginatedList } from '../../../../core/data/paginated-list.model';
|
|
||||||
import { RelationshipService } from '../../../../core/data/relationship.service';
|
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
|
||||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
|
||||||
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
|
||||||
import { MetadataMap } from '../../../../core/shared/metadata.models';
|
|
||||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
|
||||||
import { UUIDService } from '../../../../core/shared/uuid.service';
|
|
||||||
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
|
||||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
|
||||||
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
|
|
||||||
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
|
||||||
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
|
|
||||||
import { createRelationshipsObservable } from '../shared/item.component.spec';
|
|
||||||
import { UntypedItemComponent } from './untyped-item.component';
|
|
||||||
|
|
||||||
const mockItem: Item = Object.assign(new Item(), {
|
|
||||||
bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])),
|
|
||||||
metadata: new MetadataMap(),
|
|
||||||
relationships: createRelationshipsObservable()
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('UntypedItemComponent', () => {
|
|
||||||
let comp: UntypedItemComponent;
|
|
||||||
let fixture: ComponentFixture<UntypedItemComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
const mockBitstreamDataService = {
|
|
||||||
getThumbnailFor(item: Item): Observable<RemoteData<Bitstream>> {
|
|
||||||
return createSuccessfulRemoteDataObject$(new Bitstream());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [TranslateModule.forRoot({
|
|
||||||
loader: {
|
|
||||||
provide: TranslateLoader,
|
|
||||||
useClass: TranslateLoaderMock
|
|
||||||
}
|
|
||||||
})],
|
|
||||||
declarations: [UntypedItemComponent, GenericItemPageFieldComponent, TruncatePipe],
|
|
||||||
providers: [
|
|
||||||
{ provide: ItemDataService, useValue: {} },
|
|
||||||
{ provide: TruncatableService, useValue: {} },
|
|
||||||
{ provide: RelationshipService, useValue: {} },
|
|
||||||
{ provide: ObjectCacheService, useValue: {} },
|
|
||||||
{ provide: UUIDService, useValue: {} },
|
|
||||||
{ provide: Store, useValue: {} },
|
|
||||||
{ provide: RemoteDataBuildService, useValue: {} },
|
|
||||||
{ provide: CommunityDataService, useValue: {} },
|
|
||||||
{ provide: HALEndpointService, useValue: {} },
|
|
||||||
{ provide: NotificationsService, useValue: {} },
|
|
||||||
{ provide: HttpClient, useValue: {} },
|
|
||||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
|
||||||
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
|
||||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
|
||||||
],
|
|
||||||
|
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
|
||||||
}).overrideComponent(UntypedItemComponent, {
|
|
||||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
|
||||||
}).compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
fixture = TestBed.createComponent(UntypedItemComponent);
|
|
||||||
comp = fixture.componentInstance;
|
|
||||||
comp.object = mockItem;
|
|
||||||
fixture.detectChanges();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should contain a component to display the date', () => {
|
|
||||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-date-field'));
|
|
||||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain a component to display the author', () => {
|
|
||||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field'));
|
|
||||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain a component to display the abstract', () => {
|
|
||||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-abstract-field'));
|
|
||||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain a component to display the uri', () => {
|
|
||||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-uri-field'));
|
|
||||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain a component to display the collections', () => {
|
|
||||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-collections'));
|
|
||||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
@@ -1 +0,0 @@
|
|||||||
@import '../+login-page/login-page.component.scss';
|
|
@@ -1 +0,0 @@
|
|||||||
@import '../+search-page/search.component.scss';
|
|
@@ -3,6 +3,10 @@ import { getAccessControlModuleRoute } from '../app-routing-paths';
|
|||||||
|
|
||||||
export const GROUP_EDIT_PATH = 'groups';
|
export const GROUP_EDIT_PATH = 'groups';
|
||||||
|
|
||||||
|
export function getGroupsRoute() {
|
||||||
|
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH).toString();
|
||||||
|
}
|
||||||
|
|
||||||
export function getGroupEditRoute(id: string) {
|
export function getGroupEditRoute(id: string) {
|
||||||
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH, id).toString();
|
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH, id).toString();
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,9 @@ import { GroupFormComponent } from './group-registry/group-form/group-form.compo
|
|||||||
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
||||||
import { GROUP_EDIT_PATH } from './access-control-routing-paths';
|
import { GROUP_EDIT_PATH } from './access-control-routing-paths';
|
||||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
import { GroupPageGuard } from './group-registry/group-page.guard';
|
||||||
|
import { GroupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
|
||||||
|
import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -15,7 +18,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
|
|||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
},
|
},
|
||||||
data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' }
|
data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' },
|
||||||
|
canActivate: [SiteAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: GROUP_EDIT_PATH,
|
path: GROUP_EDIT_PATH,
|
||||||
@@ -23,7 +27,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
|
|||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
},
|
},
|
||||||
data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' }
|
data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' },
|
||||||
|
canActivate: [GroupAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${GROUP_EDIT_PATH}/newGroup`,
|
path: `${GROUP_EDIT_PATH}/newGroup`,
|
||||||
@@ -31,7 +36,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
|
|||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
},
|
},
|
||||||
data: { title: 'admin.access-control.groups.title.addGroup', breadcrumbKey: 'admin.access-control.groups.addGroup' }
|
data: { title: 'admin.access-control.groups.title.addGroup', breadcrumbKey: 'admin.access-control.groups.addGroup' },
|
||||||
|
canActivate: [GroupAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${GROUP_EDIT_PATH}/:groupId`,
|
path: `${GROUP_EDIT_PATH}/:groupId`,
|
||||||
@@ -39,7 +45,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
|
|||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
},
|
},
|
||||||
data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' }
|
data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' },
|
||||||
|
canActivate: [GroupPageGuard]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
|
@@ -52,15 +52,17 @@
|
|||||||
<table id="groups" class="table table-striped table-hover table-bordered">
|
<table id="groups" class="table table-striped table-hover table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||||
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||||
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let group of (groups | async)?.payload?.page">
|
<tr *ngFor="let group of (groups | async)?.payload?.page">
|
||||||
<td>{{group.id}}</td>
|
<td class="align-middle">{{group.id}}</td>
|
||||||
<td><a (click)="groupsDataService.startEditingNewGroup(group)"
|
<td class="align-middle"><a (click)="groupsDataService.startEditingNewGroup(group)"
|
||||||
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
|
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
|
||||||
|
<td class="align-middle">{{(group.object | async)?.payload?.name}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@@ -32,6 +32,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
|||||||
import { RequestService } from '../../../core/data/request.service';
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-eperson-form',
|
selector: 'ds-eperson-form',
|
||||||
@@ -272,7 +273,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
}),
|
}),
|
||||||
switchMap(([eperson, findListOptions]) => {
|
switchMap(([eperson, findListOptions]) => {
|
||||||
if (eperson != null) {
|
if (eperson != null) {
|
||||||
return this.groupsDataService.findAllByHref(eperson._links.groups.href, findListOptions);
|
return this.groupsDataService.findAllByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object'));
|
||||||
}
|
}
|
||||||
return observableOf(undefined);
|
return observableOf(undefined);
|
||||||
})
|
})
|
||||||
|
@@ -210,4 +210,11 @@ describe('GroupFormComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ngOnDestroy', () => {
|
||||||
|
it('does NOT call router.navigate', () => {
|
||||||
|
component.ngOnDestroy();
|
||||||
|
expect(router.navigate).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -14,11 +14,11 @@ import {
|
|||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
of as observableOf,
|
||||||
Subscription
|
Subscription,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { catchError, map, switchMap, take } from 'rxjs/operators';
|
import { catchError, map, switchMap, take, filter } from 'rxjs/operators';
|
||||||
import { getCollectionEditRolesRoute } from '../../../+collection-page/collection-page-routing-paths';
|
import { getCollectionEditRolesRoute } from '../../../collection-page/collection-page-routing-paths';
|
||||||
import { getCommunityEditRolesRoute } from '../../../+community-page/community-page-routing-paths';
|
import { getCommunityEditRolesRoute } from '../../../community-page/community-page-routing-paths';
|
||||||
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
|
||||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
@@ -34,7 +34,8 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
|||||||
import {
|
import {
|
||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
getFirstSucceededRemoteData,
|
getFirstSucceededRemoteData,
|
||||||
getFirstCompletedRemoteData
|
getFirstCompletedRemoteData,
|
||||||
|
getFirstSucceededRemoteDataPayload
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||||
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
|
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
|
||||||
@@ -65,6 +66,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
* Dynamic models for the inputs of form
|
* Dynamic models for the inputs of form
|
||||||
*/
|
*/
|
||||||
groupName: DynamicInputModel;
|
groupName: DynamicInputModel;
|
||||||
|
groupCommunity: DynamicInputModel;
|
||||||
groupDescription: DynamicTextAreaModel;
|
groupDescription: DynamicTextAreaModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,16 +127,16 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
public AlertTypeEnum = AlertType;
|
public AlertTypeEnum = AlertType;
|
||||||
|
|
||||||
constructor(public groupDataService: GroupDataService,
|
constructor(public groupDataService: GroupDataService,
|
||||||
private ePersonDataService: EPersonDataService,
|
private ePersonDataService: EPersonDataService,
|
||||||
private dSpaceObjectDataService: DSpaceObjectDataService,
|
private dSpaceObjectDataService: DSpaceObjectDataService,
|
||||||
private formBuilderService: FormBuilderService,
|
private formBuilderService: FormBuilderService,
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
private authorizationService: AuthorizationDataService,
|
private authorizationService: AuthorizationDataService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
public requestService: RequestService) {
|
public requestService: RequestService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -160,8 +162,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
observableCombineLatest(
|
observableCombineLatest(
|
||||||
this.translateService.get(`${this.messagePrefix}.groupName`),
|
this.translateService.get(`${this.messagePrefix}.groupName`),
|
||||||
|
this.translateService.get(`${this.messagePrefix}.groupCommunity`),
|
||||||
this.translateService.get(`${this.messagePrefix}.groupDescription`)
|
this.translateService.get(`${this.messagePrefix}.groupDescription`)
|
||||||
).subscribe(([groupName, groupDescription]) => {
|
).subscribe(([groupName, groupCommunity, groupDescription]) => {
|
||||||
this.groupName = new DynamicInputModel({
|
this.groupName = new DynamicInputModel({
|
||||||
id: 'groupName',
|
id: 'groupName',
|
||||||
label: groupName,
|
label: groupName,
|
||||||
@@ -171,6 +174,13 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
|
this.groupCommunity = new DynamicInputModel({
|
||||||
|
id: 'groupCommunity',
|
||||||
|
label: groupCommunity,
|
||||||
|
name: 'groupCommunity',
|
||||||
|
required: false,
|
||||||
|
readOnly: true,
|
||||||
|
});
|
||||||
this.groupDescription = new DynamicTextAreaModel({
|
this.groupDescription = new DynamicTextAreaModel({
|
||||||
id: 'groupDescription',
|
id: 'groupDescription',
|
||||||
label: groupDescription,
|
label: groupDescription,
|
||||||
@@ -185,17 +195,36 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
this.subs.push(
|
this.subs.push(
|
||||||
observableCombineLatest(
|
observableCombineLatest(
|
||||||
this.groupDataService.getActiveGroup(),
|
this.groupDataService.getActiveGroup(),
|
||||||
this.canEdit$
|
this.canEdit$,
|
||||||
).subscribe(([activeGroup, canEdit]) => {
|
this.groupDataService.getActiveGroup()
|
||||||
|
.pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload())))
|
||||||
|
).subscribe(([activeGroup, canEdit, linkedObject]) => {
|
||||||
|
|
||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
this.groupBeingEdited = activeGroup;
|
this.groupBeingEdited = activeGroup;
|
||||||
this.formGroup.patchValue({
|
|
||||||
groupName: activeGroup != null ? activeGroup.name : '',
|
if (linkedObject?.name) {
|
||||||
groupDescription: activeGroup != null ? activeGroup.firstMetadataValue('dc.description') : '',
|
this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity);
|
||||||
});
|
this.formGroup.patchValue({
|
||||||
if (!canEdit || activeGroup.permanent) {
|
groupName: activeGroup.name,
|
||||||
this.formGroup.disable();
|
groupCommunity: linkedObject?.name ?? '',
|
||||||
|
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.formModel = [
|
||||||
|
this.groupName,
|
||||||
|
this.groupDescription,
|
||||||
|
];
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
groupName: activeGroup.name,
|
||||||
|
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!canEdit || activeGroup.permanent) {
|
||||||
|
this.formGroup.disable();
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -405,7 +434,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
@HostListener('window:beforeunload')
|
@HostListener('window:beforeunload')
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.onCancel();
|
this.groupDataService.cancelEditGroup();
|
||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,11 +446,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
if (hasValue(group) && hasValue(group._links.object.href)) {
|
||||||
return this.getLinkedDSO(group).pipe(
|
return this.getLinkedDSO(group).pipe(
|
||||||
map((rd: RemoteData<DSpaceObject>) => {
|
map((rd: RemoteData<DSpaceObject>) => {
|
||||||
if (hasValue(rd) && hasValue(rd.payload)) {
|
return hasValue(rd) && hasValue(rd.payload);
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
catchError(() => observableOf(false)),
|
catchError(() => observableOf(false)),
|
||||||
);
|
);
|
||||||
|
@@ -38,17 +38,22 @@
|
|||||||
<table id="epersonsSearch" class="table table-striped table-hover table-bordered">
|
<table id="epersonsSearch" class="table table-striped table-hover table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||||
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.identity' | translate}}</th>
|
||||||
|
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let ePerson of (ePeopleSearchDtos | async)?.page">
|
<tr *ngFor="let ePerson of (ePeopleSearchDtos | async)?.page">
|
||||||
<td>{{ePerson.eperson.id}}</td>
|
<td class="align-middle">{{ePerson.eperson.id}}</td>
|
||||||
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
<td class="align-middle"><a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
||||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.eperson.name}}</a></td>
|
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.eperson.name}}</a></td>
|
||||||
<td>
|
<td class="align-middle">
|
||||||
|
{{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}<br/>
|
||||||
|
{{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button *ngIf="(ePerson.memberOfGroup)"
|
<button *ngIf="(ePerson.memberOfGroup)"
|
||||||
(click)="deleteMemberFromGroup(ePerson)"
|
(click)="deleteMemberFromGroup(ePerson)"
|
||||||
@@ -91,17 +96,22 @@
|
|||||||
<table id="ePeopleMembersOfGroup" class="table table-striped table-hover table-bordered">
|
<table id="ePeopleMembersOfGroup" class="table table-striped table-hover table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||||
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.identity' | translate}}</th>
|
||||||
|
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let ePerson of (ePeopleMembersOfGroupDtos | async)?.page">
|
<tr *ngFor="let ePerson of (ePeopleMembersOfGroupDtos | async)?.page">
|
||||||
<td>{{ePerson.eperson.id}}</td>
|
<td class="align-middle">{{ePerson.eperson.id}}</td>
|
||||||
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
<td class="align-middle"><a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
||||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.eperson.name}}</a></td>
|
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.eperson.name}}</a></td>
|
||||||
<td>
|
<td class="align-middle">
|
||||||
|
{{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}<br/>
|
||||||
|
{{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="align-middle">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button (click)="deleteMemberFromGroup(ePerson)"
|
<button (click)="deleteMemberFromGroup(ePerson)"
|
||||||
class="btn btn-outline-danger btn-sm"
|
class="btn btn-outline-danger btn-sm"
|
||||||
|
@@ -35,17 +35,19 @@
|
|||||||
<table id="groupsSearch" class="table table-striped table-hover table-bordered">
|
<table id="groupsSearch" class="table table-striped table-hover table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||||
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||||
|
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let group of (searchResults$ | async)?.payload?.page">
|
<tr *ngFor="let group of (searchResults$ | async)?.payload?.page">
|
||||||
<td>{{group.id}}</td>
|
<td class="align-middle">{{group.id}}</td>
|
||||||
<td><a (click)="groupDataService.startEditingNewGroup(group)"
|
<td class="align-middle"><a (click)="groupDataService.startEditingNewGroup(group)"
|
||||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
|
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
|
||||||
<td>
|
<td class="align-middle">{{(group.object | async)?.payload?.name}}</td>
|
||||||
|
<td class="align-middle">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button *ngIf="(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
<button *ngIf="(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
||||||
(click)="deleteSubgroupFromGroup(group)"
|
(click)="deleteSubgroupFromGroup(group)"
|
||||||
@@ -88,17 +90,19 @@
|
|||||||
<table id="subgroupsOfGroup" class="table table-striped table-hover table-bordered">
|
<table id="subgroupsOfGroup" class="table table-striped table-hover table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||||
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||||
|
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let group of (subGroups$ | async)?.payload?.page">
|
<tr *ngFor="let group of (subGroups$ | async)?.payload?.page">
|
||||||
<td>{{group.id}}</td>
|
<td class="align-middle">{{group.id}}</td>
|
||||||
<td><a (click)="groupDataService.startEditingNewGroup(group)"
|
<td class="align-middle"><a (click)="groupDataService.startEditingNewGroup(group)"
|
||||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
|
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
|
||||||
<td>
|
<td class="align-middle">{{(group.object | async)?.payload?.name}}</td>
|
||||||
|
<td class="align-middle">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button (click)="deleteSubgroupFromGroup(group)"
|
<button (click)="deleteSubgroupFromGroup(group)"
|
||||||
class="btn btn-outline-danger btn-sm deleteButton"
|
class="btn btn-outline-danger btn-sm deleteButton"
|
||||||
|
@@ -17,6 +17,7 @@ import { NotificationsService } from '../../../../shared/notifications/notificat
|
|||||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||||
import { NoContent } from '../../../../core/shared/NoContent.model';
|
import { NoContent } from '../../../../core/shared/NoContent.model';
|
||||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||||
|
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keys to keep track of specific subscriptions
|
* Keys to keep track of specific subscriptions
|
||||||
@@ -117,7 +118,10 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
|||||||
switchMap((config) => this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, {
|
switchMap((config) => this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, {
|
||||||
currentPage: config.currentPage,
|
currentPage: config.currentPage,
|
||||||
elementsPerPage: config.pageSize
|
elementsPerPage: config.pageSize
|
||||||
}
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
followLink('object')
|
||||||
))
|
))
|
||||||
).subscribe((rd: RemoteData<PaginatedList<Group>>) => {
|
).subscribe((rd: RemoteData<PaginatedList<Group>>) => {
|
||||||
this.subGroups$.next(rd);
|
this.subGroups$.next(rd);
|
||||||
@@ -217,7 +221,8 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
|||||||
switchMap((config) => this.groupDataService.searchGroups(this.currentSearchQuery, {
|
switchMap((config) => this.groupDataService.searchGroups(this.currentSearchQuery, {
|
||||||
currentPage: config.currentPage,
|
currentPage: config.currentPage,
|
||||||
elementsPerPage: config.pageSize
|
elementsPerPage: config.pageSize
|
||||||
}))
|
}, true, true, followLink('object')
|
||||||
|
))
|
||||||
).subscribe((rd: RemoteData<PaginatedList<Group>>) => {
|
).subscribe((rd: RemoteData<PaginatedList<Group>>) => {
|
||||||
this.searchResults$.next(rd);
|
this.searchResults$.next(rd);
|
||||||
}));
|
}));
|
||||||
|
@@ -0,0 +1,83 @@
|
|||||||
|
import { GroupPageGuard } from './group-page.guard';
|
||||||
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ActivatedRouteSnapshot, Router } from '@angular/router';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
|
describe('GroupPageGuard', () => {
|
||||||
|
const groupsEndpointUrl = 'https://test.org/api/eperson/groups';
|
||||||
|
const groupUuid = '0d6f89df-f95a-4829-943c-f21f434fb892';
|
||||||
|
const groupEndpointUrl = `${groupsEndpointUrl}/${groupUuid}`;
|
||||||
|
const routeSnapshotWithGroupId = {
|
||||||
|
params: {
|
||||||
|
groupId: groupUuid,
|
||||||
|
}
|
||||||
|
} as unknown as ActivatedRouteSnapshot;
|
||||||
|
|
||||||
|
let guard: GroupPageGuard;
|
||||||
|
let halEndpointService: HALEndpointService;
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let router: Router;
|
||||||
|
let authService: AuthService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
halEndpointService = jasmine.createSpyObj(['getEndpoint']);
|
||||||
|
(halEndpointService as any).getEndpoint.and.returnValue(observableOf(groupsEndpointUrl));
|
||||||
|
|
||||||
|
authorizationService = jasmine.createSpyObj(['isAuthorized']);
|
||||||
|
// NOTE: value is set in beforeEach
|
||||||
|
|
||||||
|
router = jasmine.createSpyObj(['parseUrl']);
|
||||||
|
(router as any).parseUrl.and.returnValue = {};
|
||||||
|
|
||||||
|
authService = jasmine.createSpyObj(['isAuthenticated']);
|
||||||
|
(authService as any).isAuthenticated.and.returnValue(observableOf(true));
|
||||||
|
|
||||||
|
guard = new GroupPageGuard(halEndpointService, authorizationService, router, authService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(guard).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canActivate', () => {
|
||||||
|
describe('when the current user can manage the group', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(authorizationService as any).isAuthorized.and.returnValue(observableOf(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true', (done) => {
|
||||||
|
guard.canActivate(
|
||||||
|
routeSnapshotWithGroupId, { url: 'current-url'} as any
|
||||||
|
).subscribe((result) => {
|
||||||
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanManageGroup, groupEndpointUrl, undefined
|
||||||
|
);
|
||||||
|
expect(result).toBeTrue();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the current user can not manage the group', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(authorizationService as any).isAuthorized.and.returnValue(observableOf(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return true', (done) => {
|
||||||
|
guard.canActivate(
|
||||||
|
routeSnapshotWithGroupId, { url: 'current-url'} as any
|
||||||
|
).subscribe((result) => {
|
||||||
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanManageGroup, groupEndpointUrl, undefined
|
||||||
|
);
|
||||||
|
expect(result).not.toBeTrue();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
35
src/app/access-control/group-registry/group-page.guard.ts
Normal file
35
src/app/access-control/group-registry/group-page.guard.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { SomeFeatureAuthorizationGuard } from '../../core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard';
|
||||||
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class GroupPageGuard extends SomeFeatureAuthorizationGuard {
|
||||||
|
|
||||||
|
protected groupsEndpoint = 'groups';
|
||||||
|
|
||||||
|
constructor(protected halEndpointService: HALEndpointService,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected authService: AuthService) {
|
||||||
|
super(authorizationService, router, authService);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> {
|
||||||
|
return observableOf([FeatureID.CanManageGroup]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||||
|
return this.halEndpointService.getEndpoint(this.groupsEndpoint).pipe(
|
||||||
|
map(groupsUrl => `${groupsUrl}/${route?.params?.groupId}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -33,9 +33,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ds-loading *ngIf="searching$ | async"></ds-loading>
|
<ds-loading *ngIf="loading$ | async"></ds-loading>
|
||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(searching$ | async)"
|
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(loading$ | async)"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="pageInfoState$"
|
[pageInfoState]="pageInfoState$"
|
||||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="col">{{messagePrefix + 'table.id' | translate}}</th>
|
<th scope="col">{{messagePrefix + 'table.id' | translate}}</th>
|
||||||
<th scope="col">{{messagePrefix + 'table.name' | translate}}</th>
|
<th scope="col">{{messagePrefix + 'table.name' | translate}}</th>
|
||||||
|
<th scope="col">{{messagePrefix + 'table.collectionOrCommunity' | translate}}</th>
|
||||||
<th scope="col">{{messagePrefix + 'table.members' | translate}}</th>
|
<th scope="col">{{messagePrefix + 'table.members' | translate}}</th>
|
||||||
<th>{{messagePrefix + 'table.edit' | translate}}</th>
|
<th>{{messagePrefix + 'table.edit' | translate}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -56,14 +57,27 @@
|
|||||||
<tr *ngFor="let groupDto of (groupsDto$ | async)?.page">
|
<tr *ngFor="let groupDto of (groupsDto$ | async)?.page">
|
||||||
<td>{{groupDto.group.id}}</td>
|
<td>{{groupDto.group.id}}</td>
|
||||||
<td>{{groupDto.group.name}}</td>
|
<td>{{groupDto.group.name}}</td>
|
||||||
|
<td>{{(groupDto.group.object | async)?.payload?.name}}</td>
|
||||||
<td>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td>
|
<td>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button [routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
|
<ng-container [ngSwitch]="groupDto.ableToEdit">
|
||||||
class="btn btn-outline-primary btn-sm"
|
<button *ngSwitchCase="true"
|
||||||
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: groupDto.group.name} }}">
|
[routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
|
||||||
<i class="fas fa-edit fa-fw"></i>
|
class="btn btn-outline-primary btn-sm btn-edit"
|
||||||
</button>
|
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: groupDto.group.name} }}"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
<button *ngSwitchCase="false"
|
||||||
|
[disabled]="true"
|
||||||
|
class="btn btn-outline-primary btn-sm btn-edit"
|
||||||
|
placement="left"
|
||||||
|
[ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
|
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
|
||||||
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm"
|
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm"
|
||||||
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: groupDto.group.name} }}">
|
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: groupDto.group.name} }}">
|
||||||
|
@@ -30,6 +30,7 @@ import { routeServiceStub } from '../../shared/testing/route-service.stub';
|
|||||||
import { RouterMock } from '../../shared/mocks/router.mock';
|
import { RouterMock } from '../../shared/mocks/router.mock';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
describe('GroupRegistryComponent', () => {
|
describe('GroupRegistryComponent', () => {
|
||||||
let component: GroupsRegistryComponent;
|
let component: GroupsRegistryComponent;
|
||||||
@@ -43,6 +44,26 @@ describe('GroupRegistryComponent', () => {
|
|||||||
let mockEPeople;
|
let mockEPeople;
|
||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set authorizationService.isAuthorized to return the following values.
|
||||||
|
* @param isAdmin whether or not the current user is an admin.
|
||||||
|
* @param canManageGroup whether or not the current user can manage all groups.
|
||||||
|
*/
|
||||||
|
const setIsAuthorized = (isAdmin: boolean, canManageGroup: boolean) => {
|
||||||
|
(authorizationService as any).isAuthorized.and.callFake((featureId?: FeatureID) => {
|
||||||
|
switch (featureId) {
|
||||||
|
case FeatureID.AdministratorOf:
|
||||||
|
return observableOf(isAdmin);
|
||||||
|
case FeatureID.CanManageGroup:
|
||||||
|
return observableOf(canManageGroup);
|
||||||
|
case FeatureID.CanDelete:
|
||||||
|
return observableOf(true);
|
||||||
|
default:
|
||||||
|
throw new Error(`setIsAuthorized: this fake implementation does not support ${featureId}.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
mockGroups = [GroupMock, GroupMock2];
|
mockGroups = [GroupMock, GroupMock2];
|
||||||
mockEPeople = [EPersonMock, EPersonMock2];
|
mockEPeople = [EPersonMock, EPersonMock2];
|
||||||
@@ -131,9 +152,9 @@ describe('GroupRegistryComponent', () => {
|
|||||||
return createSuccessfulRemoteDataObject$(undefined);
|
return createSuccessfulRemoteDataObject$(undefined);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
|
||||||
isAuthorized: observableOf(true)
|
authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']);
|
||||||
});
|
setIsAuthorized(true, true);
|
||||||
paginationService = new PaginationServiceStub();
|
paginationService = new PaginationServiceStub();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||||
@@ -180,6 +201,88 @@ describe('GroupRegistryComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should display community/collection name if present', () => {
|
||||||
|
const collectionNamesFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(3)'));
|
||||||
|
expect(collectionNamesFound.length).toEqual(2);
|
||||||
|
expect(collectionNamesFound[0].nativeElement.textContent).toEqual('');
|
||||||
|
expect(collectionNamesFound[1].nativeElement.textContent).toEqual('testgroupid2objectName');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edit buttons', () => {
|
||||||
|
describe('when the user is a general admin', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
// NOTE: setting canManageGroup to false should not matter, since isAdmin takes priority
|
||||||
|
setIsAuthorized(true, false);
|
||||||
|
|
||||||
|
// force rerender after setup changes
|
||||||
|
component.search({ query: '' });
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should be active', () => {
|
||||||
|
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||||
|
expect(editButtonsFound.length).toEqual(2);
|
||||||
|
editButtonsFound.forEach((editButtonFound) => {
|
||||||
|
expect(editButtonFound.nativeElement.disabled).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not check the canManageGroup permissions', () => {
|
||||||
|
expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanManageGroup, mockGroups[0].self
|
||||||
|
);
|
||||||
|
expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanManageGroup, mockGroups[0].self, undefined // treated differently
|
||||||
|
);
|
||||||
|
expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanManageGroup, mockGroups[1].self
|
||||||
|
);
|
||||||
|
expect(authorizationService.isAuthorized).not.toHaveBeenCalledWith(
|
||||||
|
FeatureID.CanManageGroup, mockGroups[1].self, undefined // treated differently
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user can edit the groups', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
setIsAuthorized(false, true);
|
||||||
|
|
||||||
|
// force rerender after setup changes
|
||||||
|
component.search({ query: '' });
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should be active', () => {
|
||||||
|
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||||
|
expect(editButtonsFound.length).toEqual(2);
|
||||||
|
editButtonsFound.forEach((editButtonFound) => {
|
||||||
|
expect(editButtonFound.nativeElement.disabled).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the user can not edit the groups', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
setIsAuthorized(false, false);
|
||||||
|
|
||||||
|
// force rerender after setup changes
|
||||||
|
component.search({ query: '' });
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not be active', () => {
|
||||||
|
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||||
|
expect(editButtonsFound.length).toEqual(2);
|
||||||
|
editButtonsFound.forEach((editButtonFound) => {
|
||||||
|
expect(editButtonFound.nativeElement.disabled).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('search', () => {
|
describe('search', () => {
|
||||||
describe('when searching with query', () => {
|
describe('when searching with query', () => {
|
||||||
let groupIdsFound;
|
let groupIdsFound;
|
||||||
|
@@ -9,7 +9,7 @@ import {
|
|||||||
of as observableOf,
|
of as observableOf,
|
||||||
Subscription
|
Subscription
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { catchError, map, switchMap, take } from 'rxjs/operators';
|
import { catchError, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
@@ -35,6 +35,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
|||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { NoContent } from '../../core/shared/NoContent.model';
|
import { NoContent } from '../../core/shared/NoContent.model';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-groups-registry',
|
selector: 'ds-groups-registry',
|
||||||
@@ -75,7 +76,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* A boolean representing if a search is pending
|
* A boolean representing if a search is pending
|
||||||
*/
|
*/
|
||||||
searching$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
// Current search in groups registry
|
// Current search in groups registry
|
||||||
currentSearchQuery: string;
|
currentSearchQuery: string;
|
||||||
@@ -118,12 +119,12 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
* @param data Contains query param
|
* @param data Contains query param
|
||||||
*/
|
*/
|
||||||
search(data: any) {
|
search(data: any) {
|
||||||
this.searching$.next(true);
|
|
||||||
if (hasValue(this.searchSub)) {
|
if (hasValue(this.searchSub)) {
|
||||||
this.searchSub.unsubscribe();
|
this.searchSub.unsubscribe();
|
||||||
this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub);
|
this.subs = this.subs.filter((sub: Subscription) => sub !== this.searchSub);
|
||||||
}
|
}
|
||||||
this.searchSub = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
this.searchSub = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||||
|
tap(() => this.loading$.next(true)),
|
||||||
switchMap((paginationOptions) => {
|
switchMap((paginationOptions) => {
|
||||||
const query: string = data.query;
|
const query: string = data.query;
|
||||||
if (query != null && this.currentSearchQuery !== query) {
|
if (query != null && this.currentSearchQuery !== query) {
|
||||||
@@ -132,8 +133,8 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
return this.groupService.searchGroups(this.currentSearchQuery.trim(), {
|
return this.groupService.searchGroups(this.currentSearchQuery.trim(), {
|
||||||
currentPage: paginationOptions.currentPage,
|
currentPage: paginationOptions.currentPage,
|
||||||
elementsPerPage: paginationOptions.pageSize
|
elementsPerPage: paginationOptions.pageSize,
|
||||||
});
|
}, true, true, followLink('object'));
|
||||||
}),
|
}),
|
||||||
getAllSucceededRemoteData(),
|
getAllSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
@@ -141,39 +142,53 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
if (groups.page.length === 0) {
|
if (groups.page.length === 0) {
|
||||||
return observableOf(buildPaginatedList(groups.pageInfo, []));
|
return observableOf(buildPaginatedList(groups.pageInfo, []));
|
||||||
}
|
}
|
||||||
return observableCombineLatest(groups.page.map((group: Group) => {
|
return this.authorizationService.isAuthorized(FeatureID.AdministratorOf).pipe(
|
||||||
if (!this.deletedGroupsIds.includes(group.id)) {
|
switchMap((isSiteAdmin: boolean) => {
|
||||||
return observableCombineLatest([
|
return observableCombineLatest(groups.page.map((group: Group) => {
|
||||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined),
|
if (hasValue(group) && !this.deletedGroupsIds.includes(group.id)) {
|
||||||
this.hasLinkedDSO(group),
|
return observableCombineLatest([
|
||||||
this.getSubgroups(group),
|
this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self),
|
||||||
this.getMembers(group)
|
this.canManageGroup$(isSiteAdmin, group),
|
||||||
]).pipe(
|
this.hasLinkedDSO(group),
|
||||||
map(([isAuthorized, hasLinkedDSO, subgroups, members]:
|
this.getSubgroups(group),
|
||||||
[boolean, boolean, RemoteData<PaginatedList<Group>>, RemoteData<PaginatedList<EPerson>>]) => {
|
this.getMembers(group)
|
||||||
const groupDtoModel: GroupDtoModel = new GroupDtoModel();
|
]).pipe(
|
||||||
groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO;
|
map(([canDelete, canManageGroup, hasLinkedDSO, subgroups, members]:
|
||||||
groupDtoModel.group = group;
|
[boolean, boolean, boolean, RemoteData<PaginatedList<Group>>, RemoteData<PaginatedList<EPerson>>]) => {
|
||||||
groupDtoModel.subgroups = subgroups.payload;
|
const groupDtoModel: GroupDtoModel = new GroupDtoModel();
|
||||||
groupDtoModel.epersons = members.payload;
|
groupDtoModel.ableToDelete = canDelete && !hasLinkedDSO;
|
||||||
return groupDtoModel;
|
groupDtoModel.ableToEdit = canManageGroup;
|
||||||
}
|
groupDtoModel.group = group;
|
||||||
)
|
groupDtoModel.subgroups = subgroups.payload;
|
||||||
);
|
groupDtoModel.epersons = members.payload;
|
||||||
}
|
return groupDtoModel;
|
||||||
})).pipe(map((dtos: GroupDtoModel[]) => {
|
}
|
||||||
return buildPaginatedList(groups.pageInfo, dtos);
|
)
|
||||||
}));
|
);
|
||||||
|
}
|
||||||
|
})).pipe(map((dtos: GroupDtoModel[]) => {
|
||||||
|
return buildPaginatedList(groups.pageInfo, dtos);
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
);
|
||||||
})
|
})
|
||||||
).subscribe((value: PaginatedList<GroupDtoModel>) => {
|
).subscribe((value: PaginatedList<GroupDtoModel>) => {
|
||||||
this.groupsDto$.next(value);
|
this.groupsDto$.next(value);
|
||||||
this.pageInfoState$.next(value.pageInfo);
|
this.pageInfoState$.next(value.pageInfo);
|
||||||
this.searching$.next(false);
|
this.loading$.next(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.subs.push(this.searchSub);
|
this.subs.push(this.searchSub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canManageGroup$(isSiteAdmin: boolean, group: Group): Observable<boolean> {
|
||||||
|
if (isSiteAdmin) {
|
||||||
|
return observableOf(true);
|
||||||
|
} else {
|
||||||
|
return this.authorizationService.isAuthorized(FeatureID.CanManageGroup, group.self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete Group
|
* Delete Group
|
||||||
*/
|
*/
|
||||||
|
@@ -6,10 +6,7 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
|
||||||
import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
|
import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
|
||||||
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
@@ -22,12 +19,9 @@ describe('MetadataImportPageComponent', () => {
|
|||||||
let comp: MetadataImportPageComponent;
|
let comp: MetadataImportPageComponent;
|
||||||
let fixture: ComponentFixture<MetadataImportPageComponent>;
|
let fixture: ComponentFixture<MetadataImportPageComponent>;
|
||||||
|
|
||||||
let user;
|
|
||||||
|
|
||||||
let notificationService: NotificationsServiceStub;
|
let notificationService: NotificationsServiceStub;
|
||||||
let scriptService: any;
|
let scriptService: any;
|
||||||
let router;
|
let router;
|
||||||
let authService;
|
|
||||||
let locationStub;
|
let locationStub;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
@@ -37,13 +31,6 @@ describe('MetadataImportPageComponent', () => {
|
|||||||
invoke: createSuccessfulRemoteDataObject$({ processId: '45' })
|
invoke: createSuccessfulRemoteDataObject$({ processId: '45' })
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
user = Object.assign(new EPerson(), {
|
|
||||||
id: 'userId',
|
|
||||||
email: 'user@test.com'
|
|
||||||
});
|
|
||||||
authService = jasmine.createSpyObj('authService', {
|
|
||||||
getAuthenticatedUserFromStore: observableOf(user)
|
|
||||||
});
|
|
||||||
router = jasmine.createSpyObj('router', {
|
router = jasmine.createSpyObj('router', {
|
||||||
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||||
});
|
});
|
||||||
@@ -65,7 +52,6 @@ describe('MetadataImportPageComponent', () => {
|
|||||||
{ provide: NotificationsService, useValue: notificationService },
|
{ provide: NotificationsService, useValue: notificationService },
|
||||||
{ provide: ScriptDataService, useValue: scriptService },
|
{ provide: ScriptDataService, useValue: scriptService },
|
||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
{ provide: AuthService, useValue: authService },
|
|
||||||
{ provide: Location, useValue: locationStub },
|
{ provide: Location, useValue: locationStub },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
@@ -107,9 +93,8 @@ describe('MetadataImportPageComponent', () => {
|
|||||||
proceed.click();
|
proceed.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
it('metadata-import script is invoked with its -e currentUserEmail, -f fileName and the mockFile', () => {
|
it('metadata-import script is invoked with -f fileName and the mockFile', () => {
|
||||||
const parameterValues: ProcessParameter[] = [
|
const parameterValues: ProcessParameter[] = [
|
||||||
Object.assign(new ProcessParameter(), { name: '-e', value: user.email }),
|
|
||||||
Object.assign(new ProcessParameter(), { name: '-f', value: 'filename.txt' }),
|
Object.assign(new ProcessParameter(), { name: '-f', value: 'filename.txt' }),
|
||||||
];
|
];
|
||||||
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
|
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
|
@@ -23,20 +23,14 @@ import { getProcessDetailRoute } from '../../process-page/process-page-routing.p
|
|||||||
/**
|
/**
|
||||||
* Component that represents a metadata import page for administrators
|
* Component that represents a metadata import page for administrators
|
||||||
*/
|
*/
|
||||||
export class MetadataImportPageComponent implements OnInit {
|
export class MetadataImportPageComponent {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current value of the file
|
* The current value of the file
|
||||||
*/
|
*/
|
||||||
fileObject: File;
|
fileObject: File;
|
||||||
|
|
||||||
/**
|
public constructor(private location: Location,
|
||||||
* The authenticated user's email
|
|
||||||
*/
|
|
||||||
private currentUserEmail$: Observable<string>;
|
|
||||||
|
|
||||||
public constructor(protected authService: AuthService,
|
|
||||||
private location: Location,
|
|
||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
private scriptDataService: ScriptDataService,
|
private scriptDataService: ScriptDataService,
|
||||||
@@ -51,15 +45,6 @@ export class MetadataImportPageComponent implements OnInit {
|
|||||||
this.fileObject = file;
|
this.fileObject = file;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method provided by Angular. Invoked after the constructor.
|
|
||||||
*/
|
|
||||||
ngOnInit() {
|
|
||||||
this.currentUserEmail$ = this.authService.getAuthenticatedUserFromStore().pipe(
|
|
||||||
map((user: EPerson) => user.email)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When return button is pressed go to previous location
|
* When return button is pressed go to previous location
|
||||||
*/
|
*/
|
||||||
@@ -68,22 +53,17 @@ export class MetadataImportPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts import-metadata script with -e currentUserEmail -f fileName (and the selected file)
|
* Starts import-metadata script with -f fileName (and the selected file)
|
||||||
*/
|
*/
|
||||||
public importMetadata() {
|
public importMetadata() {
|
||||||
if (this.fileObject == null) {
|
if (this.fileObject == null) {
|
||||||
this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile'));
|
this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile'));
|
||||||
} else {
|
} else {
|
||||||
this.currentUserEmail$.pipe(
|
const parameterValues: ProcessParameter[] = [
|
||||||
switchMap((email: string) => {
|
Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }),
|
||||||
if (isNotEmpty(email)) {
|
];
|
||||||
const parameterValues: ProcessParameter[] = [
|
|
||||||
Object.assign(new ProcessParameter(), { name: '-e', value: email }),
|
this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]).pipe(
|
||||||
Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }),
|
|
||||||
];
|
|
||||||
return this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
).subscribe((rd: RemoteData<Process>) => {
|
).subscribe((rd: RemoteData<Process>) => {
|
||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user