mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-10 19:43:04 +00:00
Merge remote-tracking branch 'remotes/origin/main' into upgrade_angular10
# Conflicts: # package.json # server.ts # src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts # src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts # src/app/+admin/admin-access-control/group-registry/groups-registry.component.spec.ts # src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts # src/app/+collection-page/collection-page.component.ts # src/app/+item-page/edit-item-page/item-page-reinstate.guard.ts # src/app/+item-page/edit-item-page/item-page-withdraw.guard.ts # src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts # src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts # src/app/+item-page/item-page-administrator.guard.ts # src/app/app-routing.module.ts # src/app/community-list-page/community-list-service.ts # src/app/core/data/entity-type.service.ts # src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.spec.ts # src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.spec.ts # src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.ts # src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts # src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts # src/app/core/data/object-updates/object-updates.reducer.ts # src/app/core/shared/operators.ts # src/app/process-page/detail/process-detail.component.ts # src/app/shared/comcol-forms/delete-comcol-page/delete-comcol-page.component.ts # src/app/shared/dso-selector/dso-selector/dso-selector.component.ts # src/app/shared/input-suggestions/filter-suggestions/filter-input-suggestions.component.ts # src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts # src/app/shared/shared.module.ts # yarn.lock
This commit is contained in:
@@ -17,10 +17,9 @@ coverage:
|
|||||||
# Configuration for patch-level checks. This checks the relative coverage of the new PR code ONLY.
|
# Configuration for patch-level checks. This checks the relative coverage of the new PR code ONLY.
|
||||||
patch:
|
patch:
|
||||||
default:
|
default:
|
||||||
# For each PR, make sure the coverage of the new code is within 1% of current overall coverage.
|
# Enable informational mode, which just provides info to reviewers & always passes
|
||||||
# We let 'patch' be more lenient as we only require *project* coverage to not drop significantly.
|
# https://docs.codecov.io/docs/commit-status#section-informational
|
||||||
target: auto
|
informational: true
|
||||||
threshold: 1%
|
|
||||||
|
|
||||||
# Turn PR comments "off". This feature adds the code coverage summary as a
|
# Turn PR comments "off". This feature adds the code coverage summary as a
|
||||||
# comment on each PR. See https://docs.codecov.io/docs/pull-request-comments
|
# comment on each PR. See https://docs.codecov.io/docs/pull-request-comments
|
||||||
|
@@ -12,3 +12,6 @@ trim_trailing_whitespace = true
|
|||||||
[*.md]
|
[*.md]
|
||||||
insert_final_newline = false
|
insert_final_newline = false
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.ts]
|
||||||
|
quote_type = single
|
||||||
|
87
.github/workflows/build.yml
vendored
Normal file
87
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# DSpace Continuous Integration/Build via GitHub Actions
|
||||||
|
# Concepts borrowed from
|
||||||
|
# https://docs.github.com/en/free-pro-team@latest/actions/guides/building-and-testing-nodejs
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
# Run this Build for all pushes / PRs to current branch
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
# The ci step will test the dspace-angular code against DSpace REST.
|
||||||
|
# Direct that step to utilize a DSpace REST service that has been started in docker.
|
||||||
|
DSPACE_REST_HOST: localhost
|
||||||
|
DSPACE_REST_PORT: 8080
|
||||||
|
DSPACE_REST_NAMESPACE: '/server'
|
||||||
|
DSPACE_REST_SSL: false
|
||||||
|
strategy:
|
||||||
|
# Create a matrix of Node versions to test against (in parallel)
|
||||||
|
matrix:
|
||||||
|
node-version: [10.x, 12.x]
|
||||||
|
# Do NOT exit immediately if one matrix job fails
|
||||||
|
fail-fast: false
|
||||||
|
# These are the actual CI steps to perform per job
|
||||||
|
steps:
|
||||||
|
# https://github.com/actions/checkout
|
||||||
|
- name: Checkout codebase
|
||||||
|
uses: actions/checkout@v1
|
||||||
|
|
||||||
|
# https://github.com/actions/setup-node
|
||||||
|
- name: Install Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
|
- name: Install latest Chrome (for e2e tests)
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get --only-upgrade install google-chrome-stable -y
|
||||||
|
google-chrome --version
|
||||||
|
|
||||||
|
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
|
||||||
|
- name: Get Yarn cache directory
|
||||||
|
id: yarn-cache-dir-path
|
||||||
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
- name: Cache Yarn dependencies
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
# Cache entire Yarn cache directory (see previous step)
|
||||||
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
# Cache key is hash of yarn.lock. Therefore changes to yarn.lock will invalidate cache
|
||||||
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: ${{ runner.os }}-yarn-
|
||||||
|
|
||||||
|
- name: Install Yarn dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run lint
|
||||||
|
run: yarn run lint
|
||||||
|
|
||||||
|
- name: Run build
|
||||||
|
run: yarn run build:prod
|
||||||
|
|
||||||
|
- name: Run specs (unit tests)
|
||||||
|
run: yarn run test:headless
|
||||||
|
|
||||||
|
# Using docker-compose start backend using CI configuration
|
||||||
|
# and load assetstore from a cached copy
|
||||||
|
- name: Start DSpace REST Backend via Docker (for e2e tests)
|
||||||
|
run: |
|
||||||
|
docker-compose -f ./docker/docker-compose-ci.yml up -d
|
||||||
|
docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
|
||||||
|
docker container ls
|
||||||
|
|
||||||
|
- name: Run e2e tests (integration tests)
|
||||||
|
run: yarn run e2e:ci
|
||||||
|
|
||||||
|
- name: Shutdown Docker containers
|
||||||
|
run: docker-compose -f ./docker/docker-compose-ci.yml down
|
||||||
|
|
||||||
|
# NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286
|
||||||
|
# Upload coverage reports to Codecov (for Node v12 only)
|
||||||
|
# https://github.com/codecov/codecov-action
|
||||||
|
- name: Upload coverage to Codecov.io
|
||||||
|
uses: codecov/codecov-action@v1
|
||||||
|
if: matrix.node-version == '12.x'
|
66
.travis.yml
66
.travis.yml
@@ -1,66 +0,0 @@
|
|||||||
os: linux
|
|
||||||
dist: bionic
|
|
||||||
language: node_js
|
|
||||||
|
|
||||||
# Enable caching for yarn & node_modules
|
|
||||||
cache:
|
|
||||||
yarn: true
|
|
||||||
|
|
||||||
node_js:
|
|
||||||
- "10"
|
|
||||||
- "12"
|
|
||||||
|
|
||||||
# Install latest chrome (for e2e headless testing). Run an update if needed.
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
sources:
|
|
||||||
- google-chrome
|
|
||||||
packages:
|
|
||||||
- google-chrome-stable
|
|
||||||
update: true
|
|
||||||
|
|
||||||
env:
|
|
||||||
# The ci step will test the dspace-angular code against DSpace REST.
|
|
||||||
# Direct that step to utilize a DSpace REST service that has been started in docker.
|
|
||||||
DSPACE_REST_HOST: localhost
|
|
||||||
DSPACE_REST_PORT: 8080
|
|
||||||
DSPACE_REST_NAMESPACE: '/server'
|
|
||||||
DSPACE_REST_SSL: false
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
# Check our versions of everything
|
|
||||||
- echo "Check versions"
|
|
||||||
- yarn -v
|
|
||||||
- docker-compose -v
|
|
||||||
- google-chrome-stable --version
|
|
||||||
|
|
||||||
install:
|
|
||||||
# Start up a test DSpace 7 REST backend using the entities database dump
|
|
||||||
- docker-compose -f ./docker/docker-compose-travis.yml up -d
|
|
||||||
# Use the dspace-cli image to populate the assetstore. Triggers a discovery and oai update
|
|
||||||
- docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
|
|
||||||
# Install all local dependencies (retry if initially fails)
|
|
||||||
- travis_retry yarn install
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- echo "Check Docker containers"
|
|
||||||
- docker container ls
|
|
||||||
# The following line could be enabled to verify that the rest server is responding.
|
|
||||||
#- echo "Check REST API available (via Docker)"
|
|
||||||
#- curl http://localhost:8080/server/
|
|
||||||
|
|
||||||
script:
|
|
||||||
# build app and run all tests
|
|
||||||
- ng lint || travis_terminate 1;
|
|
||||||
- travis_wait yarn run build:prod || travis_terminate 1;
|
|
||||||
- yarn test:headless || travis_terminate 1;
|
|
||||||
- yarn run e2e:ci || travis_terminate 1;
|
|
||||||
|
|
||||||
after_script:
|
|
||||||
# Shutdown docker after everything runs
|
|
||||||
- docker-compose -f ./docker/docker-compose-travis.yml down
|
|
||||||
|
|
||||||
# After a successful build and test (see 'script'), send code coverage reports to codecov.io
|
|
||||||
# NOTE: As there's no need to send coverage multiple times, we only run this for one version of node.
|
|
||||||
after_success:
|
|
||||||
- if [ "$TRAVIS_NODE_VERSION" = "12" ]; then bash <(curl -s https://codecov.io/bash); fi
|
|
@@ -1,4 +1,4 @@
|
|||||||
[](https://travis-ci.com/DSpace/dspace-angular) [](https://codecov.io/gh/DSpace/dspace-angular) [](https://github.com/angular/universal)
|
[](https://github.com/DSpace/dspace-angular/actions?query=workflow%3ABuild) [](https://codecov.io/gh/DSpace/dspace-angular) [](https://github.com/angular/universal)
|
||||||
|
|
||||||
dspace-angular
|
dspace-angular
|
||||||
==============
|
==============
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
# Docker Compose for running the DSpace backend for e2e testing in CI
|
||||||
networks:
|
networks:
|
||||||
dspacenet:
|
dspacenet:
|
||||||
services:
|
services:
|
@@ -42,7 +42,7 @@
|
|||||||
"clean:bld": "rimraf build",
|
"clean:bld": "rimraf build",
|
||||||
"clean:node": "rimraf node_modules",
|
"clean:node": "rimraf node_modules",
|
||||||
"clean:prod": "yarn run clean:coverage && yarn run clean:doc && yarn run clean:dist && yarn run clean:log && yarn run clean:json && yarn run clean:bld",
|
"clean:prod": "yarn run clean:coverage && yarn run clean:doc && yarn run clean:dist && yarn run clean:log && yarn run clean:json && yarn run clean:bld",
|
||||||
"clean": "yarn run clean:prod && yarn run clean:node && yarn run clean:env",
|
"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"
|
"postinstall": "ngcc"
|
||||||
@@ -91,6 +91,7 @@
|
|||||||
"debug-loader": "^0.0.1",
|
"debug-loader": "^0.0.1",
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"express-rate-limit": "^5.1.3",
|
||||||
"fast-json-patch": "^2.0.7",
|
"fast-json-patch": "^2.0.7",
|
||||||
"file-saver": "^1.3.8",
|
"file-saver": "^1.3.8",
|
||||||
"filesize": "^6.1.0",
|
"filesize": "^6.1.0",
|
||||||
|
@@ -54,13 +54,6 @@ import(environmentFilePath)
|
|||||||
function generateEnvironmentFile(file: GlobalConfig): void {
|
function generateEnvironmentFile(file: GlobalConfig): void {
|
||||||
file.production = production;
|
file.production = production;
|
||||||
buildBaseUrls(file);
|
buildBaseUrls(file);
|
||||||
|
|
||||||
// TODO remove workaround in beta 5
|
|
||||||
if (file.rest.nameSpace.match("(.*)/api/?$") !== null) {
|
|
||||||
file.rest.nameSpace = getNameSpace(file.rest.nameSpace);
|
|
||||||
console.log(colors.white.bgMagenta.bold(`The rest.nameSpace property in your environment file or in your DSPACE_REST_NAMESPACE environment variable ends with '/api'.\nThis is deprecated. As '/api' isn't configurable on the rest side, it shouldn't be repeated in every environment file.\nPlease change the rest nameSpace to '${file.rest.nameSpace}'`));
|
|
||||||
}
|
|
||||||
|
|
||||||
const contents = `export const environment = ` + JSON.stringify(file);
|
const contents = `export const environment = ` + JSON.stringify(file);
|
||||||
writeFile(targetPath, contents, (err) => {
|
writeFile(targetPath, contents, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -119,16 +112,5 @@ function getPort(port: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getNameSpace(nameSpace: string): string {
|
function getNameSpace(nameSpace: string): string {
|
||||||
// TODO remove workaround in beta 5
|
return nameSpace ? nameSpace.charAt(0) === '/' ? nameSpace : '/' + nameSpace : '';
|
||||||
const apiMatches = nameSpace.match("(.*)/api/?$");
|
|
||||||
if (apiMatches != null) {
|
|
||||||
let newValue = '/'
|
|
||||||
if (hasValue(apiMatches[1])) {
|
|
||||||
newValue = apiMatches[1];
|
|
||||||
}
|
|
||||||
return newValue;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return nameSpace ? nameSpace.charAt(0) === '/' ? nameSpace : '/' + nameSpace : '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
17
server.ts
17
server.ts
@@ -35,6 +35,7 @@ import { environment } from './src/environments/environment';
|
|||||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||||
import { hasValue, hasNoValue } from './src/app/shared/empty.util';
|
import { hasValue, hasNoValue } from './src/app/shared/empty.util';
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
import { APP_BASE_HREF } from '@angular/common';
|
||||||
|
import { UIServerConfig } from './src/config/ui-server-config.interface';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Set path for the browser application's dist folder
|
* Set path for the browser application's dist folder
|
||||||
@@ -122,6 +123,19 @@ export function app() {
|
|||||||
res.status(404).send('data requests are not yet supported');
|
res.status(404).send('data requests are not yet supported');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the rateLimiter property is present
|
||||||
|
* When it is present, the rateLimiter will be enabled. When it is undefined, the rateLimiter will be disabled.
|
||||||
|
*/
|
||||||
|
if (hasValue((environment.ui as UIServerConfig).rateLimiter)) {
|
||||||
|
const RateLimit = require('express-rate-limit');
|
||||||
|
const limiter = new RateLimit({
|
||||||
|
windowMs: (environment.ui as UIServerConfig).rateLimiter.windowMs,
|
||||||
|
max: (environment.ui as UIServerConfig).rateLimiter.max
|
||||||
|
});
|
||||||
|
app.use(limiter);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Serve static resources (images, i18n messages, …)
|
* Serve static resources (images, i18n messages, …)
|
||||||
*/
|
*/
|
||||||
@@ -235,8 +249,9 @@ if (environment.ui.ssl) {
|
|||||||
certificate: certificate
|
certificate: certificate
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
console.warn('Disabling certificate validation and proceeding with a self-signed certificate. If this is a production server, it is recommended that you configure a valid certificate instead.');
|
||||||
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation]
|
||||||
|
|
||||||
pem.createCertificate({
|
pem.createCertificate({
|
||||||
days: 1,
|
days: 1,
|
||||||
|
@@ -13,12 +13,12 @@ import { GROUP_EDIT_PATH } from './admin-access-control-routing-paths';
|
|||||||
{
|
{
|
||||||
path: `${GROUP_EDIT_PATH}/:groupId`,
|
path: `${GROUP_EDIT_PATH}/:groupId`,
|
||||||
component: GroupFormComponent,
|
component: GroupFormComponent,
|
||||||
data: {title: 'admin.registries.schema.title'}
|
data: {title: 'admin.access-control.groups.title.singleGroup'}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${GROUP_EDIT_PATH}/newGroup`,
|
path: `${GROUP_EDIT_PATH}/newGroup`,
|
||||||
component: GroupFormComponent,
|
component: GroupFormComponent,
|
||||||
data: {title: 'admin.registries.schema.title'}
|
data: {title: 'admin.access-control.groups.title.addGroup'}
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
|
@@ -40,7 +40,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(ePeopleDto$ | async)?.totalElements > 0"
|
*ngIf="(pageInfoState$ | async)?.totalElements > 0"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="pageInfoState$"
|
[pageInfoState]="pageInfoState$"
|
||||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||||
|
@@ -44,7 +44,7 @@ describe('EPeopleRegistryComponent', () => {
|
|||||||
activeEPerson: null,
|
activeEPerson: null,
|
||||||
allEpeople: mockEPeople,
|
allEpeople: mockEPeople,
|
||||||
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
|
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, currentPage: 1 }), this.allEpeople));
|
||||||
},
|
},
|
||||||
getActiveEPerson(): Observable<EPerson> {
|
getActiveEPerson(): Observable<EPerson> {
|
||||||
return observableOf(this.activeEPerson);
|
return observableOf(this.activeEPerson);
|
||||||
@@ -54,18 +54,18 @@ describe('EPeopleRegistryComponent', () => {
|
|||||||
const result = this.allEpeople.find((ePerson: EPerson) => {
|
const result = this.allEpeople.find((ePerson: EPerson) => {
|
||||||
return ePerson.email === query;
|
return ePerson.email === query;
|
||||||
});
|
});
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: [result].length, totalElements: [result].length, totalPages: 1, currentPage: 1 }), [result]));
|
||||||
}
|
}
|
||||||
if (scope === 'metadata') {
|
if (scope === 'metadata') {
|
||||||
if (query === '') {
|
if (query === '') {
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, currentPage: 1 }), this.allEpeople));
|
||||||
}
|
}
|
||||||
const result = this.allEpeople.find((ePerson: EPerson) => {
|
const result = this.allEpeople.find((ePerson: EPerson) => {
|
||||||
return (ePerson.name.includes(query) || ePerson.email.includes(query));
|
return (ePerson.name.includes(query) || ePerson.email.includes(query));
|
||||||
});
|
});
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: [result].length, totalElements: [result].length, totalPages: 1, currentPage: 1 }), [result]));
|
||||||
}
|
}
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, currentPage: 1 }), this.allEpeople));
|
||||||
},
|
},
|
||||||
deleteEPerson(ePerson: EPerson): Observable<boolean> {
|
deleteEPerson(ePerson: EPerson): Observable<boolean> {
|
||||||
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
|
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
|
||||||
|
@@ -140,6 +140,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
elementsPerPage: this.config.pageSize
|
elementsPerPage: this.config.pageSize
|
||||||
}).subscribe((peopleRD) => {
|
}).subscribe((peopleRD) => {
|
||||||
this.ePeople$.next(peopleRD);
|
this.ePeople$.next(peopleRD);
|
||||||
|
this.pageInfoState$.next(peopleRD.payload.pageInfo);
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="group-form row">
|
<div class="group-form row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"
|
||||||
|
[content]="messagePrefix + '.alert.permanent'"></ds-alert>
|
||||||
|
<ds-alert *ngIf="!(canEdit$ | async) && (groupDataService.getActiveGroup() | async)" [type]="AlertTypeEnum.Warning"
|
||||||
|
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: (getLinkedDSO(groupBeingEdited) | async)?.payload?.name, comcol: (getLinkedDSO(groupBeingEdited) | async)?.payload?.type, comcolEditRolesRoute: (getLinkedEditRolesRoute(groupBeingEdited) | async) })">
|
||||||
|
</ds-alert>
|
||||||
|
|
||||||
<div *ngIf="groupDataService.getActiveGroup() | async; then editheader; else createHeader"></div>
|
<div *ngIf="groupDataService.getActiveGroup() | async; then editheader; else createHeader"></div>
|
||||||
|
|
||||||
@@ -18,10 +23,18 @@
|
|||||||
[formLayout]="formLayout"
|
[formLayout]="formLayout"
|
||||||
(cancel)="onCancel()"
|
(cancel)="onCancel()"
|
||||||
(submitForm)="onSubmit()">
|
(submitForm)="onSubmit()">
|
||||||
|
<div *ngIf="groupBeingEdited != null" class="row">
|
||||||
|
<button class="btn btn-light delete-button" [disabled]="!(canEdit$ | async) || groupBeingEdited.permanent"
|
||||||
|
(click)="delete()">
|
||||||
|
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
|
|
||||||
<ds-members-list *ngIf="groupBeingEdited != null" [messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
|
<ds-members-list *ngIf="groupBeingEdited != null"
|
||||||
<ds-subgroups-list *ngIf="groupBeingEdited != null" [messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
|
[messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
|
||||||
|
<ds-subgroups-list *ngIf="groupBeingEdited != null"
|
||||||
|
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button [routerLink]="[this.groupDataService.getGroupRegistryRouterLink()]"
|
<button [routerLink]="[this.groupDataService.getGroupRegistryRouterLink()]"
|
||||||
|
@@ -13,11 +13,14 @@ import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-d
|
|||||||
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||||
import { RestResponse } from '../../../../core/cache/response.models';
|
import { RestResponse } from '../../../../core/cache/response.models';
|
||||||
import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service';
|
||||||
|
import { DSpaceObjectDataService } from '../../../../core/data/dspace-object-data.service';
|
||||||
|
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||||
import { Group } from '../../../../core/eperson/models/group.model';
|
import { Group } from '../../../../core/eperson/models/group.model';
|
||||||
|
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||||
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
||||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
import { UUIDService } from '../../../../core/shared/uuid.service';
|
import { UUIDService } from '../../../../core/shared/uuid.service';
|
||||||
@@ -39,6 +42,9 @@ describe('GroupFormComponent', () => {
|
|||||||
let builderService: FormBuilderService;
|
let builderService: FormBuilderService;
|
||||||
let ePersonDataServiceStub: any;
|
let ePersonDataServiceStub: any;
|
||||||
let groupsDataServiceStub: any;
|
let groupsDataServiceStub: any;
|
||||||
|
let dsoDataServiceStub: any;
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let notificationService: NotificationsServiceStub;
|
||||||
let router;
|
let router;
|
||||||
|
|
||||||
let groups;
|
let groups;
|
||||||
@@ -73,6 +79,9 @@ describe('GroupFormComponent', () => {
|
|||||||
editGroup(group: Group) {
|
editGroup(group: Group) {
|
||||||
this.activeGroup = group;
|
this.activeGroup = group;
|
||||||
},
|
},
|
||||||
|
updateGroup(group: Group) {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
cancelEditGroup(): void {
|
cancelEditGroup(): void {
|
||||||
this.activeGroup = null;
|
this.activeGroup = null;
|
||||||
},
|
},
|
||||||
@@ -87,9 +96,18 @@ describe('GroupFormComponent', () => {
|
|||||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), []));
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), []));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true)
|
||||||
|
});
|
||||||
|
dsoDataServiceStub = {
|
||||||
|
findByHref(href: string): Observable<RemoteData<DSpaceObject>> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
builderService = getMockFormBuilderService();
|
builderService = getMockFormBuilderService();
|
||||||
translateService = getMockTranslateService();
|
translateService = getMockTranslateService();
|
||||||
router = new RouterMock();
|
router = new RouterMock();
|
||||||
|
notificationService = new NotificationsServiceStub();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot({
|
||||||
@@ -103,7 +121,8 @@ describe('GroupFormComponent', () => {
|
|||||||
providers: [GroupFormComponent,
|
providers: [GroupFormComponent,
|
||||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||||
{ provide: GroupDataService, useValue: groupsDataServiceStub },
|
{ provide: GroupDataService, useValue: groupsDataServiceStub },
|
||||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
{ provide: DSpaceObjectDataService, useValue: dsoDataServiceStub },
|
||||||
|
{ provide: NotificationsService, useValue: notificationService },
|
||||||
{ provide: FormBuilderService, useValue: builderService },
|
{ provide: FormBuilderService, useValue: builderService },
|
||||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||||
{ provide: HttpClient, useValue: {} },
|
{ provide: HttpClient, useValue: {} },
|
||||||
@@ -114,6 +133,7 @@ describe('GroupFormComponent', () => {
|
|||||||
{ provide: HALEndpointService, useValue: {} },
|
{ provide: HALEndpointService, useValue: {} },
|
||||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }), params: observableOf({}) } },
|
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }), params: observableOf({}) } },
|
||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -147,6 +167,34 @@ describe('GroupFormComponent', () => {
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
describe('with active Group', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf(expected));
|
||||||
|
spyOn(groupsDataServiceStub, 'updateGroup').and.returnValue(observableOf(new RestResponse(true, 200, 'OK')));
|
||||||
|
component.groupName.value = 'newGroupName';
|
||||||
|
component.onSubmit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit the existing group using the correct new values', async(() => {
|
||||||
|
const expected2 = Object.assign(new Group(), {
|
||||||
|
name: 'newGroupName',
|
||||||
|
metadata: {
|
||||||
|
'dc.description': [
|
||||||
|
{
|
||||||
|
value: groupDescription
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
it('should emit success notification', () => {
|
||||||
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
|
})
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output } from '@angular/core';
|
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
import { FormGroup } from '@angular/forms';
|
import { FormGroup } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {
|
import {
|
||||||
DynamicFormControlModel,
|
DynamicFormControlModel,
|
||||||
DynamicFormLayout,
|
DynamicFormLayout,
|
||||||
@@ -8,17 +9,30 @@ import {
|
|||||||
DynamicTextAreaModel
|
DynamicTextAreaModel
|
||||||
} from '@ng-dynamic-forms/core';
|
} from '@ng-dynamic-forms/core';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { combineLatest, Subscription } from 'rxjs';
|
import { ObservedValueOf, combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||||
import { take } from 'rxjs/operators';
|
import { catchError, map, switchMap, take } from 'rxjs/operators';
|
||||||
|
import { getCollectionEditRolesRoute } from '../../../../+collection-page/collection-page-routing-paths';
|
||||||
|
import { getCommunityEditRolesRoute } from '../../../../+community-page/community-page-routing-paths';
|
||||||
import { RestResponse } from '../../../../core/cache/response.models';
|
import { RestResponse } from '../../../../core/cache/response.models';
|
||||||
|
import { DSpaceObjectDataService } from '../../../../core/data/dspace-object-data.service';
|
||||||
|
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { RequestService } from '../../../../core/data/request.service';
|
||||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||||
import { Group } from '../../../../core/eperson/models/group.model';
|
import { Group } from '../../../../core/eperson/models/group.model';
|
||||||
|
import { Collection } from '../../../../core/shared/collection.model';
|
||||||
|
import { Community } from '../../../../core/shared/community.model';
|
||||||
|
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||||
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||||
|
import { AlertType } from '../../../../shared/alert/aletr-type';
|
||||||
|
import { ConfirmationModalComponent } from '../../../../shared/confirmation-modal/confirmation-modal.component';
|
||||||
import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
|
||||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
|
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-group-form',
|
selector: 'ds-group-form',
|
||||||
@@ -88,22 +102,51 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
groupBeingEdited: Group;
|
groupBeingEdited: Group;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable whether or not the logged in user is allowed to delete the Group & doesn't have a linked object (community / collection linked to workspace group
|
||||||
|
*/
|
||||||
|
canEdit$: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AlertType enumeration
|
||||||
|
* @type {AlertType}
|
||||||
|
*/
|
||||||
|
public AlertTypeEnum = AlertType;
|
||||||
|
|
||||||
constructor(public groupDataService: GroupDataService,
|
constructor(public groupDataService: GroupDataService,
|
||||||
private ePersonDataService: EPersonDataService,
|
private ePersonDataService: EPersonDataService,
|
||||||
|
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 modalService: NgbModal,
|
||||||
|
public requestService: RequestService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.initialisePage();
|
||||||
|
}
|
||||||
|
|
||||||
|
initialisePage() {
|
||||||
this.subs.push(this.route.params.subscribe((params) => {
|
this.subs.push(this.route.params.subscribe((params) => {
|
||||||
this.setActiveGroup(params.groupId);
|
this.setActiveGroup(params.groupId);
|
||||||
}));
|
}));
|
||||||
combineLatest(
|
this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
|
||||||
|
switchMap((group: Group) => {
|
||||||
|
return observableCombineLatest(
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined),
|
||||||
|
this.hasLinkedDSO(group),
|
||||||
|
(isAuthorized: ObservedValueOf<Observable<boolean>>, hasLinkedDSO: ObservedValueOf<Observable<boolean>>) => {
|
||||||
|
return isAuthorized && !hasLinkedDSO;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
);
|
||||||
|
observableCombineLatest(
|
||||||
this.translateService.get(`${this.messagePrefix}.groupName`),
|
this.translateService.get(`${this.messagePrefix}.groupName`),
|
||||||
this.translateService.get(`${this.messagePrefix}.groupDescription`),
|
this.translateService.get(`${this.messagePrefix}.groupDescription`)
|
||||||
).subscribe(([groupName, groupDescription]) => {
|
).subscribe(([groupName, groupDescription]) => {
|
||||||
this.groupName = new DynamicInputModel({
|
this.groupName = new DynamicInputModel({
|
||||||
id: 'groupName',
|
id: 'groupName',
|
||||||
@@ -122,21 +165,26 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
this.formModel = [
|
this.formModel = [
|
||||||
this.groupName,
|
this.groupName,
|
||||||
this.groupDescription
|
this.groupDescription,
|
||||||
];
|
];
|
||||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
this.subs.push(this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => {
|
this.subs.push(
|
||||||
if (activeGroup != null) {
|
observableCombineLatest(
|
||||||
this.groupBeingEdited = activeGroup;
|
this.groupDataService.getActiveGroup(),
|
||||||
this.formGroup.patchValue({
|
this.canEdit$
|
||||||
groupName: activeGroup != null ? activeGroup.name : '',
|
).subscribe(([activeGroup, canEdit]) => {
|
||||||
groupDescription: activeGroup != null ? activeGroup.firstMetadataValue('dc.description') : '',
|
if (activeGroup != null) {
|
||||||
});
|
this.groupBeingEdited = activeGroup;
|
||||||
if (activeGroup.permanent) {
|
this.formGroup.patchValue({
|
||||||
this.formGroup.get('groupName').disable();
|
groupName: activeGroup != null ? activeGroup.name : '',
|
||||||
|
groupDescription: activeGroup != null ? activeGroup.firstMetadataValue('dc.description') : '',
|
||||||
|
});
|
||||||
|
if (!canEdit || activeGroup.permanent) {
|
||||||
|
this.formGroup.disable();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}));
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +219,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
if (group === null) {
|
if (group === null) {
|
||||||
this.createNewGroup(values);
|
this.createNewGroup(values);
|
||||||
} else {
|
} else {
|
||||||
this.editGroup(group, values);
|
this.editGroup(group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -192,6 +240,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
if (isNotEmpty(resp.resourceSelfLinks)) {
|
if (isNotEmpty(resp.resourceSelfLinks)) {
|
||||||
const groupSelfLink = resp.resourceSelfLinks[0];
|
const groupSelfLink = resp.resourceSelfLinks[0];
|
||||||
this.setActiveGroupWithLink(groupSelfLink);
|
this.setActiveGroupWithLink(groupSelfLink);
|
||||||
|
this.groupDataService.clearGroupsRequests();
|
||||||
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLinkWithID(this.groupDataService.getUUIDFromString(groupSelfLink)));
|
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLinkWithID(this.groupDataService.getUUIDFromString(groupSelfLink)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -224,14 +273,32 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* // TODO
|
* Edit existing Group based on given values from form and old Group
|
||||||
* @param group
|
* @param group Group to edit and old values contained within
|
||||||
* @param values
|
|
||||||
*/
|
*/
|
||||||
editGroup(group: Group, values) {
|
editGroup(group: Group) {
|
||||||
// TODO (backend)
|
const editedGroup = Object.assign(new Group(), {
|
||||||
console.log('TODO implement editGroup', values);
|
id: group.id,
|
||||||
this.notificationsService.error('TODO implement editGroup (not yet implemented in backend) ');
|
metadata: {
|
||||||
|
'dc.description': [
|
||||||
|
{
|
||||||
|
value: (hasValue(this.groupDescription.value) ? this.groupDescription.value : group.firstMetadataValue('dc.description'))
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
name: (hasValue(this.groupName.value) ? this.groupName.value : group.name),
|
||||||
|
_links: group._links,
|
||||||
|
});
|
||||||
|
const response = this.groupDataService.updateGroup(editedGroup);
|
||||||
|
response.pipe(take(1)).subscribe((restResponse: RestResponse) => {
|
||||||
|
if (restResponse.isSuccessful) {
|
||||||
|
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.edited.success', { name: editedGroup.name }));
|
||||||
|
this.submitForm.emit(editedGroup);
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.edited.failure', { name: editedGroup.name }));
|
||||||
|
this.cancelForm.emit();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -257,7 +324,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||||
if (activeGroup === null) {
|
if (activeGroup === null) {
|
||||||
this.groupDataService.cancelEditGroup();
|
this.groupDataService.cancelEditGroup();
|
||||||
this.groupDataService.findByHref(groupSelfLink)
|
this.groupDataService.findByHref(groupSelfLink, followLink('subgroups'), followLink('epersons'), followLink('object'))
|
||||||
.pipe(
|
.pipe(
|
||||||
getSucceededRemoteData(),
|
getSucceededRemoteData(),
|
||||||
getRemoteDataPayload())
|
getRemoteDataPayload())
|
||||||
@@ -268,6 +335,48 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the Group from the Repository. The Group will be the only that this form is showing.
|
||||||
|
* It'll either show a success or error message depending on whether the delete was successful or not.
|
||||||
|
*/
|
||||||
|
delete() {
|
||||||
|
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((group: Group) => {
|
||||||
|
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||||
|
modalRef.componentInstance.dso = group;
|
||||||
|
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header';
|
||||||
|
modalRef.componentInstance.infoLabel = this.messagePrefix + '.delete-group.modal.info';
|
||||||
|
modalRef.componentInstance.cancelLabel = this.messagePrefix + '.delete-group.modal.cancel';
|
||||||
|
modalRef.componentInstance.confirmLabel = this.messagePrefix + '.delete-group.modal.confirm';
|
||||||
|
modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => {
|
||||||
|
if (confirm) {
|
||||||
|
if (hasValue(group.id)) {
|
||||||
|
this.groupDataService.deleteGroup(group).pipe(take(1))
|
||||||
|
.subscribe(([success, optionalErrorMessage]: [boolean, string]) => {
|
||||||
|
if (success) {
|
||||||
|
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.deleted.success', { name: group.name }));
|
||||||
|
this.reset();
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(
|
||||||
|
this.translateService.get(this.messagePrefix + '.notification.deleted.failure.title', { name: group.name }),
|
||||||
|
this.translateService.get(this.messagePrefix + '.notification.deleted.failure.content', { cause: optionalErrorMessage }));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will ensure that the page gets reset and that the cache is cleared
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.groupDataService.getBrowseEndpoint().pipe(take(1)).subscribe((href: string) => {
|
||||||
|
this.requestService.removeByHrefSubstring(href);
|
||||||
|
});
|
||||||
|
this.onCancel();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||||
*/
|
*/
|
||||||
@@ -276,4 +385,58 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
this.onCancel();
|
this.onCancel();
|
||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if group has a linked object (community or collection linked to a workflow group)
|
||||||
|
* @param group
|
||||||
|
*/
|
||||||
|
hasLinkedDSO(group: Group): Observable<boolean> {
|
||||||
|
if (hasValue(group) && hasValue(group._links.object.href)) {
|
||||||
|
return this.getLinkedDSO(group).pipe(
|
||||||
|
map((rd: RemoteData<DSpaceObject>) => {
|
||||||
|
if (hasValue(rd) && hasValue(rd.payload)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
catchError(() => observableOf(false)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get group's linked object if it has one (community or collection linked to a workflow group)
|
||||||
|
* @param group
|
||||||
|
*/
|
||||||
|
getLinkedDSO(group: Group): Observable<RemoteData<DSpaceObject>> {
|
||||||
|
if (hasValue(group) && hasValue(group._links.object.href)) {
|
||||||
|
if (group.object === undefined) {
|
||||||
|
return this.dSpaceObjectDataService.findByHref(group._links.object.href);
|
||||||
|
}
|
||||||
|
return group.object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the route to the edit roles tab of the group's linked object (community or collection linked to a workflow group) if it has one
|
||||||
|
* @param group
|
||||||
|
*/
|
||||||
|
getLinkedEditRolesRoute(group: Group): Observable<string> {
|
||||||
|
if (hasValue(group) && hasValue(group._links.object.href)) {
|
||||||
|
return this.getLinkedDSO(group).pipe(
|
||||||
|
map((rd: RemoteData<DSpaceObject>) => {
|
||||||
|
if (hasValue(rd) && hasValue(rd.payload)) {
|
||||||
|
const dso = rd.payload
|
||||||
|
switch ((dso as any).type) {
|
||||||
|
case Community.type.value:
|
||||||
|
return getCommunityEditRolesRoute(rd.payload.id);
|
||||||
|
case Collection.type.value:
|
||||||
|
return getCollectionEditRolesRoute(rd.payload.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -30,10 +30,10 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(groups | async)?.payload?.totalElements > 0"
|
*ngIf="(pageInfoState$ | async)?.totalElements > 0"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="(groups | async)?.payload"
|
[pageInfoState]="pageInfoState$"
|
||||||
[collectionSize]="(groups | async)?.payload?.totalElements"
|
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true"
|
[hidePagerWhenSinglePage]="true"
|
||||||
(pageChange)="onPageChange($event)">
|
(pageChange)="onPageChange($event)">
|
||||||
@@ -50,21 +50,21 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let group of (groups | async)?.payload?.page">
|
<tr *ngFor="let groupDto of (groupsDto$ | async)?.page">
|
||||||
<td>{{group.id}}</td>
|
<td>{{groupDto.group.id}}</td>
|
||||||
<td>{{group.name}}</td>
|
<td>{{groupDto.group.name}}</td>
|
||||||
<td>{{(getMembers(group) | async)?.payload?.totalElements + (getSubgroups(group) | async)?.payload?.totalElements}}</td>
|
<td>{{(getMembers(groupDto.group) | async)?.payload?.totalElements + (getSubgroups(groupDto.group) | async)?.payload?.totalElements}}</td>
|
||||||
<!-- <td>{{getOptionalComColFromName(group.name)}}</td>-->
|
<!-- <td>{{getOptionalComColFromName(group.name)}}</td>-->
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button [routerLink]="groupService.getGroupEditPageRouterLink(group)"
|
<button [routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
|
||||||
class="btn btn-outline-primary btn-sm"
|
class="btn btn-outline-primary btn-sm"
|
||||||
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: group.name} }}">
|
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: groupDto.group.name} }}">
|
||||||
<i class="fas fa-edit fa-fw"></i>
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="!group?.permanent" (click)="deleteGroup(group)"
|
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
|
||||||
class="btn btn-outline-danger btn-sm"
|
(click)="deleteGroup(groupDto.group)" class="btn btn-outline-danger btn-sm"
|
||||||
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: group.name} }}">
|
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: groupDto.group.name} }}">
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</ds-pagination>
|
</ds-pagination>
|
||||||
|
|
||||||
<div *ngIf="(groups | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
<div *ngIf="(pageInfoState$ | async)?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||||
{{messagePrefix + 'no-items' | translate}}
|
{{messagePrefix + 'no-items' | translate}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -6,14 +6,18 @@ import { BrowserModule, By } from '@angular/platform-browser';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||||
import { Group } from '../../../core/eperson/models/group.model';
|
import { Group } from '../../../core/eperson/models/group.model';
|
||||||
import { RouteService } from '../../../core/services/route.service';
|
import { RouteService } from '../../../core/services/route.service';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock';
|
import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock';
|
||||||
@@ -30,6 +34,8 @@ describe('GroupRegistryComponent', () => {
|
|||||||
let fixture: ComponentFixture<GroupsRegistryComponent>;
|
let fixture: ComponentFixture<GroupsRegistryComponent>;
|
||||||
let ePersonDataServiceStub: any;
|
let ePersonDataServiceStub: any;
|
||||||
let groupsDataServiceStub: any;
|
let groupsDataServiceStub: any;
|
||||||
|
let dsoDataServiceStub: any;
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
|
||||||
let mockGroups;
|
let mockGroups;
|
||||||
let mockEPeople;
|
let mockEPeople;
|
||||||
@@ -41,11 +47,11 @@ describe('GroupRegistryComponent', () => {
|
|||||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
findAllByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
switch (href) {
|
switch (href) {
|
||||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons':
|
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons':
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, []));
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 0, totalPages: 0, currentPage: 1 }), []));
|
||||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/epersons':
|
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/epersons':
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, [EPersonMock]));
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 1, totalPages: 1, currentPage: 1 }), [EPersonMock]));
|
||||||
default:
|
default:
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, []));
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 0, totalPages: 0, currentPage: 1 }), []));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -54,11 +60,11 @@ describe('GroupRegistryComponent', () => {
|
|||||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
switch (href) {
|
switch (href) {
|
||||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/groups':
|
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/groups':
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, []));
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 0, totalPages: 0, currentPage: 1 }), []));
|
||||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/groups':
|
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/groups':
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, [GroupMock2]));
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 1, totalPages: 1, currentPage: 1 }), [GroupMock2]));
|
||||||
default:
|
default:
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, []));
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: 0, totalPages: 0, currentPage: 1 }), []));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getGroupEditPageRouterLink(group: Group): string {
|
getGroupEditPageRouterLink(group: Group): string {
|
||||||
@@ -69,14 +75,22 @@ describe('GroupRegistryComponent', () => {
|
|||||||
},
|
},
|
||||||
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
|
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
if (query === '') {
|
if (query === '') {
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allGroups));
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: this.allGroups.length, totalElements: this.allGroups.length, totalPages: 1, currentPage: 1 }), this.allGroups));
|
||||||
}
|
}
|
||||||
const result = this.allGroups.find((group: Group) => {
|
const result = this.allGroups.find((group: Group) => {
|
||||||
return (group.id.includes(query));
|
return (group.id.includes(query));
|
||||||
});
|
});
|
||||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: [result].length, totalElements: [result].length, totalPages: 1, currentPage: 1 }), [result]));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
dsoDataServiceStub = {
|
||||||
|
findByHref(href: string): Observable<RemoteData<DSpaceObject>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true)
|
||||||
|
});
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot({
|
||||||
@@ -90,9 +104,12 @@ describe('GroupRegistryComponent', () => {
|
|||||||
providers: [GroupsRegistryComponent,
|
providers: [GroupsRegistryComponent,
|
||||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||||
{ provide: GroupDataService, useValue: groupsDataServiceStub },
|
{ provide: GroupDataService, useValue: groupsDataServiceStub },
|
||||||
|
{ provide: DSpaceObjectDataService, useValue: dsoDataServiceStub },
|
||||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||||
{ provide: RouteService, useValue: routeServiceStub },
|
{ provide: RouteService, useValue: routeServiceStub },
|
||||||
{ provide: Router, useValue: new RouterMock() },
|
{ provide: Router, useValue: new RouterMock() },
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])}
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@@ -1,16 +1,26 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { FormBuilder } from '@angular/forms';
|
import { FormBuilder } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { Observable } from 'rxjs';
|
import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription, Observable, of as observableOf } from 'rxjs';
|
||||||
import { take } from 'rxjs/operators';
|
import { filter } from 'rxjs/internal/operators/filter';
|
||||||
|
import { ObservedValueOf } from 'rxjs/internal/types';
|
||||||
|
import { catchError, map, switchMap, take } from 'rxjs/operators';
|
||||||
|
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||||
|
import { GroupDtoModel } from '../../../core/eperson/models/group-dto.model';
|
||||||
import { Group } from '../../../core/eperson/models/group.model';
|
import { Group } from '../../../core/eperson/models/group.model';
|
||||||
import { RouteService } from '../../../core/services/route.service';
|
import { RouteService } from '../../../core/services/route.service';
|
||||||
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
|
import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||||
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
@@ -23,7 +33,7 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio
|
|||||||
* A component used for managing all existing groups within the repository.
|
* A component used for managing all existing groups within the repository.
|
||||||
* The admin can create, edit or delete groups here.
|
* The admin can create, edit or delete groups here.
|
||||||
*/
|
*/
|
||||||
export class GroupsRegistryComponent implements OnInit {
|
export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
messagePrefix = 'admin.access-control.groups.';
|
messagePrefix = 'admin.access-control.groups.';
|
||||||
|
|
||||||
@@ -37,9 +47,19 @@ export class GroupsRegistryComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of all the current groups within the repository or the result of the search
|
* A list of all the current Groups within the repository or the result of the search
|
||||||
*/
|
*/
|
||||||
groups: Observable<RemoteData<PaginatedList<Group>>>;
|
groups$: BehaviorSubject<RemoteData<PaginatedList<Group>>> = new BehaviorSubject<RemoteData<PaginatedList<Group>>>({} as any);
|
||||||
|
/**
|
||||||
|
* A BehaviorSubject with the list of GroupDtoModel objects made from the Groups in the repository or
|
||||||
|
* as the result of the search
|
||||||
|
*/
|
||||||
|
groupsDto$: BehaviorSubject<PaginatedList<GroupDtoModel>> = new BehaviorSubject<PaginatedList<GroupDtoModel>>({} as any);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An observable for the pageInfo, needed to pass to the pagination component
|
||||||
|
*/
|
||||||
|
pageInfoState$: BehaviorSubject<PageInfo> = new BehaviorSubject<PageInfo>(undefined);
|
||||||
|
|
||||||
// The search form
|
// The search form
|
||||||
searchForm;
|
searchForm;
|
||||||
@@ -47,13 +67,21 @@ export class GroupsRegistryComponent implements OnInit {
|
|||||||
// Current search in groups registry
|
// Current search in groups registry
|
||||||
currentSearchQuery: string;
|
currentSearchQuery: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of subscriptions
|
||||||
|
*/
|
||||||
|
subs: Subscription[] = [];
|
||||||
|
|
||||||
constructor(public groupService: GroupDataService,
|
constructor(public groupService: GroupDataService,
|
||||||
private ePersonDataService: EPersonDataService,
|
private ePersonDataService: EPersonDataService,
|
||||||
|
private dSpaceObjectDataService: DSpaceObjectDataService,
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
protected routeService: RouteService,
|
protected routeService: RouteService,
|
||||||
private router: Router) {
|
private router: Router,
|
||||||
|
private authorizationService: AuthorizationDataService,
|
||||||
|
public requestService: RequestService) {
|
||||||
this.currentSearchQuery = '';
|
this.currentSearchQuery = '';
|
||||||
this.searchForm = this.formBuilder.group(({
|
this.searchForm = this.formBuilder.group(({
|
||||||
query: this.currentSearchQuery,
|
query: this.currentSearchQuery,
|
||||||
@@ -84,37 +112,69 @@ export class GroupsRegistryComponent implements OnInit {
|
|||||||
this.currentSearchQuery = query;
|
this.currentSearchQuery = query;
|
||||||
this.config.currentPage = 1;
|
this.config.currentPage = 1;
|
||||||
}
|
}
|
||||||
this.groups = this.groupService.searchGroups(this.currentSearchQuery.trim(), {
|
this.subs.push(this.groupService.searchGroups(this.currentSearchQuery.trim(), {
|
||||||
currentPage: this.config.currentPage,
|
currentPage: this.config.currentPage,
|
||||||
elementsPerPage: this.config.pageSize
|
elementsPerPage: this.config.pageSize
|
||||||
});
|
}).subscribe((groupsRD: RemoteData<PaginatedList<Group>>) => {
|
||||||
|
this.groups$.next(groupsRD);
|
||||||
|
this.pageInfoState$.next(groupsRD.payload.pageInfo);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
this.subs.push(this.groups$.pipe(
|
||||||
|
getAllSucceededRemoteDataPayload(),
|
||||||
|
switchMap((groups: PaginatedList<Group>) => {
|
||||||
|
return observableCombineLatest(...groups.page.map((group: Group) => {
|
||||||
|
return observableCombineLatest(
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(group) ? group.self : undefined),
|
||||||
|
this.hasLinkedDSO(group),
|
||||||
|
(isAuthorized: ObservedValueOf<Observable<boolean>>, hasLinkedDSO: ObservedValueOf<Observable<boolean>>) => {
|
||||||
|
const groupDtoModel: GroupDtoModel = new GroupDtoModel();
|
||||||
|
groupDtoModel.ableToDelete = isAuthorized && !hasLinkedDSO;
|
||||||
|
groupDtoModel.group = group;
|
||||||
|
return groupDtoModel;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})).pipe(map((dtos: GroupDtoModel[]) => {
|
||||||
|
return new PaginatedList(groups.pageInfo, dtos);
|
||||||
|
}))
|
||||||
|
})).subscribe((value: PaginatedList<GroupDtoModel>) => {
|
||||||
|
this.groupsDto$.next(value);
|
||||||
|
this.pageInfoState$.next(value.pageInfo);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete Group
|
* Delete Group
|
||||||
*/
|
*/
|
||||||
deleteGroup(group: Group) {
|
deleteGroup(group: Group) {
|
||||||
// TODO (backend)
|
|
||||||
console.log('TODO implement editGroup', group);
|
|
||||||
this.notificationsService.error('TODO implement deleteGroup (not yet implemented in backend)');
|
|
||||||
if (hasValue(group.id)) {
|
if (hasValue(group.id)) {
|
||||||
this.groupService.deleteGroup(group).pipe(take(1)).subscribe((success: boolean) => {
|
this.groupService.deleteGroup(group).pipe(take(1))
|
||||||
if (success) {
|
.subscribe(([success, optionalErrorMessage]: [boolean, string]) => {
|
||||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.name }));
|
if (success) {
|
||||||
this.forceUpdateGroup();
|
this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.name }));
|
||||||
} else {
|
this.reset();
|
||||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + 'notification.deleted.failure', { name: group.name }));
|
} else {
|
||||||
}
|
this.notificationsService.error(
|
||||||
|
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.name }),
|
||||||
|
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.content', { cause: optionalErrorMessage }));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force-update the list of groups by first clearing the cache related to groups, then performing a new REST call
|
* This method will ensure that the page gets reset and that the cache is cleared
|
||||||
*/
|
*/
|
||||||
public forceUpdateGroup() {
|
reset() {
|
||||||
this.groupService.clearGroupsRequests();
|
this.groupService.getBrowseEndpoint().pipe(
|
||||||
this.search({ query: this.currentSearchQuery });
|
switchMap((href) => this.requestService.removeByHrefSubstring(href)),
|
||||||
|
filter((isCached) => isCached),
|
||||||
|
take(1)
|
||||||
|
).subscribe(() => {
|
||||||
|
this.cleanupSubscribes();
|
||||||
|
this.search({ query: this.currentSearchQuery });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,6 +193,23 @@ export class GroupsRegistryComponent implements OnInit {
|
|||||||
return this.groupService.findAllByHref(group._links.subgroups.href);
|
return this.groupService.findAllByHref(group._links.subgroups.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if group has a linked object (community or collection linked to a workflow group)
|
||||||
|
* @param group
|
||||||
|
*/
|
||||||
|
hasLinkedDSO(group: Group): Observable<boolean> {
|
||||||
|
return this.dSpaceObjectDataService.findByHref(group._links.object.href).pipe(
|
||||||
|
map((rd: RemoteData<DSpaceObject>) => {
|
||||||
|
if (hasValue(rd) && hasValue(rd.payload)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
catchError(() => observableOf(false)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset all input-fields to be empty and search all search
|
* Reset all input-fields to be empty and search all search
|
||||||
*/
|
*/
|
||||||
@@ -151,4 +228,15 @@ export class GroupsRegistryComponent implements OnInit {
|
|||||||
getOptionalComColFromName(groupName: string): string {
|
getOptionalComColFromName(groupName: string): string {
|
||||||
return this.groupService.getUUIDFromString(groupName);
|
return this.groupService.getUUIDFromString(groupName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsub all subscriptions
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.cleanupSubscribes();
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupSubscribes() {
|
||||||
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,7 @@ import { Collection } from '../../../../../core/shared/collection.model';
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths';
|
import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths';
|
||||||
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
|
|
||||||
describe('CollectionAdminSearchResultGridElementComponent', () => {
|
describe('CollectionAdminSearchResultGridElementComponent', () => {
|
||||||
let component: CollectionAdminSearchResultGridElementComponent;
|
let component: CollectionAdminSearchResultGridElementComponent;
|
||||||
@@ -26,6 +27,11 @@ describe('CollectionAdminSearchResultGridElementComponent', () => {
|
|||||||
searchResult.indexableObject = new Collection();
|
searchResult.indexableObject = new Collection();
|
||||||
searchResult.indexableObject.uuid = id;
|
searchResult.indexableObject.uuid = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const linkService = jasmine.createSpyObj('linkService', {
|
||||||
|
resolveLink: {}
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
init();
|
init();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -39,6 +45,7 @@ describe('CollectionAdminSearchResultGridElementComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: TruncatableService, useValue: mockTruncatableService },
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
{ provide: BitstreamDataService, useValue: {} },
|
{ provide: BitstreamDataService, useValue: {} },
|
||||||
|
{ provide: LinkService, useValue: linkService}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
@@ -14,8 +14,8 @@ import { RouterTestingModule } from '@angular/router/testing';
|
|||||||
import { CommunityAdminSearchResultGridElementComponent } from './community-admin-search-result-grid-element.component';
|
import { CommunityAdminSearchResultGridElementComponent } from './community-admin-search-result-grid-element.component';
|
||||||
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
|
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
|
||||||
import { Community } from '../../../../../core/shared/community.model';
|
import { Community } from '../../../../../core/shared/community.model';
|
||||||
import { CommunityAdminSearchResultListElementComponent } from '../../admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component';
|
|
||||||
import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths';
|
import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths';
|
||||||
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
|
|
||||||
describe('CommunityAdminSearchResultGridElementComponent', () => {
|
describe('CommunityAdminSearchResultGridElementComponent', () => {
|
||||||
let component: CommunityAdminSearchResultGridElementComponent;
|
let component: CommunityAdminSearchResultGridElementComponent;
|
||||||
@@ -29,6 +29,11 @@ describe('CommunityAdminSearchResultGridElementComponent', () => {
|
|||||||
searchResult.indexableObject = new Community();
|
searchResult.indexableObject = new Community();
|
||||||
searchResult.indexableObject.uuid = id;
|
searchResult.indexableObject.uuid = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const linkService = jasmine.createSpyObj('linkService', {
|
||||||
|
resolveLink: {}
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
init();
|
init();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -42,6 +47,7 @@ describe('CommunityAdminSearchResultGridElementComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: TruncatableService, useValue: mockTruncatableService },
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
{ provide: BitstreamDataService, useValue: {} },
|
{ provide: BitstreamDataService, useValue: {} },
|
||||||
|
{ provide: LinkService, useValue: linkService}
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
@@ -6,6 +6,7 @@ import { AuthorizationDataService } from '../core/data/feature-authorization/aut
|
|||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -16,8 +17,9 @@ import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
|||||||
export class CollectionPageAdministratorGuard extends DsoPageFeatureGuard<Collection> {
|
export class CollectionPageAdministratorGuard extends DsoPageFeatureGuard<Collection> {
|
||||||
constructor(protected resolver: CollectionPageResolver,
|
constructor(protected resolver: CollectionPageResolver,
|
||||||
protected authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
protected router: Router) {
|
protected router: Router,
|
||||||
super(resolver, authorizationService, router);
|
protected authService: AuthService) {
|
||||||
|
super(resolver, authorizationService, router, authService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -20,6 +20,11 @@ export function getCollectionCreateRoute() {
|
|||||||
return new URLCombiner(getCollectionModuleRoute(), COLLECTION_CREATE_PATH).toString();
|
return new URLCombiner(getCollectionModuleRoute(), COLLECTION_CREATE_PATH).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCollectionEditRolesRoute(id) {
|
||||||
|
return new URLCombiner(getCollectionPageRoute(id), COLLECTION_EDIT_PATH, COLLECTION_EDIT_ROLES_PATH).toString()
|
||||||
|
}
|
||||||
|
|
||||||
export const COLLECTION_CREATE_PATH = 'create';
|
export const COLLECTION_CREATE_PATH = 'create';
|
||||||
export const COLLECTION_EDIT_PATH = 'edit';
|
export const COLLECTION_EDIT_PATH = 'edit';
|
||||||
|
export const COLLECTION_EDIT_ROLES_PATH = 'roles';
|
||||||
export const ITEMTEMPLATE_PATH = 'itemtemplate';
|
export const ITEMTEMPLATE_PATH = 'itemtemplate';
|
||||||
|
@@ -3,37 +3,41 @@
|
|||||||
*ngVar="(collectionRD$ | async) as collectionRD">
|
*ngVar="(collectionRD$ | async) as collectionRD">
|
||||||
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
|
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
|
||||||
<div *ngIf="collectionRD?.payload as collection">
|
<div *ngIf="collectionRD?.payload as collection">
|
||||||
<ds-view-tracker [object]="collection"></ds-view-tracker>
|
<ds-view-tracker [object]="collection"></ds-view-tracker>
|
||||||
<header class="comcol-header border-bottom mb-4 pb-4">
|
<div class="d-flex flex-row border-bottom mb-4 pb-4">
|
||||||
|
<header class="comcol-header mr-auto">
|
||||||
<!-- Collection Name -->
|
<!-- Collection Name -->
|
||||||
<ds-comcol-page-header
|
<ds-comcol-page-header
|
||||||
[name]="collection.name">
|
[name]="collection.name">
|
||||||
</ds-comcol-page-header>
|
</ds-comcol-page-header>
|
||||||
<!-- Collection logo -->
|
<!-- Collection logo -->
|
||||||
<ds-comcol-page-logo *ngIf="logoRD$"
|
<ds-comcol-page-logo *ngIf="logoRD$"
|
||||||
[logo]="(logoRD$ | async)?.payload"
|
[logo]="(logoRD$ | async)?.payload"
|
||||||
[alternateText]="'Collection Logo'"
|
[alternateText]="'Collection Logo'"
|
||||||
[alternateText]="'Collection Logo'">
|
[alternateText]="'Collection Logo'">
|
||||||
</ds-comcol-page-logo>
|
</ds-comcol-page-logo>
|
||||||
|
|
||||||
<!-- Handle -->
|
<!-- Handle -->
|
||||||
<ds-comcol-page-handle
|
<ds-comcol-page-handle
|
||||||
[content]="collection.handle"
|
[content]="collection.handle"
|
||||||
[title]="'collection.page.handle'" >
|
[title]="'collection.page.handle'" >
|
||||||
</ds-comcol-page-handle>
|
</ds-comcol-page-handle>
|
||||||
<!-- Introductory text -->
|
<!-- Introductory text -->
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content
|
||||||
[content]="collection.introductoryText"
|
[content]="collection.introductoryText"
|
||||||
[hasInnerHtml]="true">
|
[hasInnerHtml]="true">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
<!-- News -->
|
<!-- News -->
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content
|
||||||
[content]="collection.sidebarText"
|
[content]="collection.sidebarText"
|
||||||
[hasInnerHtml]="true"
|
[hasInnerHtml]="true"
|
||||||
[title]="'collection.page.news'">
|
[title]="'collection.page.news'">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
|
</header>
|
||||||
</header>
|
<div class="pl-2">
|
||||||
|
<ds-dso-page-edit-button [pageRoutePrefix]="'collections'" [dso]="collection" [tooltipMsg]="'collection.page.edit'"></ds-dso-page-edit-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<section class="comcol-page-browse-section">
|
<section class="comcol-page-browse-section">
|
||||||
<!-- Browse-By Links -->
|
<!-- Browse-By Links -->
|
||||||
<ds-comcol-page-browse-by
|
<ds-comcol-page-browse-by
|
||||||
|
@@ -15,11 +15,16 @@ import { Bitstream } from '../core/shared/bitstream.model';
|
|||||||
import { Collection } from '../core/shared/collection.model';
|
import { Collection } from '../core/shared/collection.model';
|
||||||
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
|
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
|
||||||
import { Item } from '../core/shared/item.model';
|
import { Item } from '../core/shared/item.model';
|
||||||
import { getSucceededRemoteData, redirectOn404Or401, toDSpaceObjectListRD } from '../core/shared/operators';
|
import {
|
||||||
|
getSucceededRemoteData,
|
||||||
|
redirectOn4xx,
|
||||||
|
toDSpaceObjectListRD
|
||||||
|
} from '../core/shared/operators';
|
||||||
|
|
||||||
import { fadeIn, fadeInOut } from '../shared/animations/fade';
|
import { fadeIn, fadeInOut } from '../shared/animations/fade';
|
||||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-collection-page',
|
selector: 'ds-collection-page',
|
||||||
@@ -47,7 +52,8 @@ export class CollectionPageComponent implements OnInit {
|
|||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
private metadata: MetadataService,
|
private metadata: MetadataService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router
|
private router: Router,
|
||||||
|
private authService: AuthService,
|
||||||
) {
|
) {
|
||||||
this.paginationConfig = new PaginationComponentOptions();
|
this.paginationConfig = new PaginationComponentOptions();
|
||||||
this.paginationConfig.id = 'collection-page-pagination';
|
this.paginationConfig.id = 'collection-page-pagination';
|
||||||
@@ -59,7 +65,7 @@ export class CollectionPageComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.collectionRD$ = this.route.data.pipe(
|
this.collectionRD$ = this.route.data.pipe(
|
||||||
map((data) => data.dso as RemoteData<Collection>),
|
map((data) => data.dso as RemoteData<Collection>),
|
||||||
redirectOn404Or401(this.router),
|
redirectOn4xx(this.router, this.authService),
|
||||||
take(1)
|
take(1)
|
||||||
);
|
);
|
||||||
this.logoRD$ = this.collectionRD$.pipe(
|
this.logoRD$ = this.collectionRD$.pipe(
|
||||||
|
@@ -12,6 +12,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
|
|||||||
import { CreateCollectionPageComponent } from './create-collection-page.component';
|
import { CreateCollectionPageComponent } from './create-collection-page.component';
|
||||||
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';
|
||||||
|
import {RequestService} from '../../core/data/request.service';
|
||||||
|
|
||||||
describe('CreateCollectionPageComponent', () => {
|
describe('CreateCollectionPageComponent', () => {
|
||||||
let comp: CreateCollectionPageComponent;
|
let comp: CreateCollectionPageComponent;
|
||||||
@@ -29,7 +30,8 @@ describe('CreateCollectionPageComponent', () => {
|
|||||||
},
|
},
|
||||||
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
|
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
|
||||||
{ provide: Router, useValue: {} },
|
{ provide: Router, useValue: {} },
|
||||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||||
|
{ provide: RequestService, useValue: {}}
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@@ -7,6 +7,7 @@ import { Collection } from '../../core/shared/collection.model';
|
|||||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import {RequestService} from '../../core/data/request.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that represents the page where a user can create a new Collection
|
* Component that represents the page where a user can create a new Collection
|
||||||
@@ -26,8 +27,9 @@ export class CreateCollectionPageComponent extends CreateComColPageComponent<Col
|
|||||||
protected routeService: RouteService,
|
protected routeService: RouteService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected translate: TranslateService
|
protected translate: TranslateService,
|
||||||
|
protected requestService: RequestService
|
||||||
) {
|
) {
|
||||||
super(collectionDataService, communityDataService, routeService, router, notificationsService, translate);
|
super(collectionDataService, communityDataService, routeService, router, notificationsService, translate, requestService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,7 @@ import { of as observableOf } from 'rxjs';
|
|||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { DeleteCollectionPageComponent } from './delete-collection-page.component';
|
import { DeleteCollectionPageComponent } from './delete-collection-page.component';
|
||||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
import {RequestService} from '../../core/data/request.service';
|
||||||
|
|
||||||
describe('DeleteCollectionPageComponent', () => {
|
describe('DeleteCollectionPageComponent', () => {
|
||||||
let comp: DeleteCollectionPageComponent;
|
let comp: DeleteCollectionPageComponent;
|
||||||
@@ -22,6 +23,7 @@ describe('DeleteCollectionPageComponent', () => {
|
|||||||
{ provide: CollectionDataService, useValue: {} },
|
{ provide: CollectionDataService, useValue: {} },
|
||||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||||
{ provide: NotificationsService, useValue: {} },
|
{ provide: NotificationsService, useValue: {} },
|
||||||
|
{ provide: RequestService, useValue: {} }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@@ -5,6 +5,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
|||||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
import { Collection } from '../../core/shared/collection.model';
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import {RequestService} from '../../core/data/request.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that represents the page where a user can delete an existing Collection
|
* Component that represents the page where a user can delete an existing Collection
|
||||||
@@ -22,8 +23,9 @@ export class DeleteCollectionPageComponent extends DeleteComColPageComponent<Col
|
|||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected notifications: NotificationsService,
|
protected notifications: NotificationsService,
|
||||||
protected translate: TranslateService
|
protected translate: TranslateService,
|
||||||
|
protected requestService: RequestService
|
||||||
) {
|
) {
|
||||||
super(dsoDataService, router, route, notifications, translate);
|
super(dsoDataService, router, route, notifications, translate, requestService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,7 +17,7 @@ describe('CollectionCurateComponent', () => {
|
|||||||
let dsoNameService;
|
let dsoNameService;
|
||||||
|
|
||||||
const collection = Object.assign(new Collection(), {
|
const collection = Object.assign(new Collection(), {
|
||||||
handle: '123456789/1', metadata: {'dc.title': ['Collection Name']}
|
metadata: {'dc.title': ['Collection Name'], 'dc.identifier.uri': [ { value: '123456789/1'}]}
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
|
@@ -6,6 +6,7 @@ import { AuthorizationDataService } from '../core/data/feature-authorization/aut
|
|||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -16,8 +17,9 @@ import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
|||||||
export class CommunityPageAdministratorGuard extends DsoPageFeatureGuard<Community> {
|
export class CommunityPageAdministratorGuard extends DsoPageFeatureGuard<Community> {
|
||||||
constructor(protected resolver: CommunityPageResolver,
|
constructor(protected resolver: CommunityPageResolver,
|
||||||
protected authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
protected router: Router) {
|
protected router: Router,
|
||||||
super(resolver, authorizationService, router);
|
protected authService: AuthService) {
|
||||||
|
super(resolver, authorizationService, router, authService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { getCollectionPageRoute } from '../+collection-page/collection-page-routing-paths';
|
||||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
|
|
||||||
export const COMMUNITY_PARENT_PARAMETER = 'parent';
|
export const COMMUNITY_PARENT_PARAMETER = 'parent';
|
||||||
@@ -20,5 +21,10 @@ export function getCommunityCreateRoute() {
|
|||||||
return new URLCombiner(getCommunityModuleRoute(), COMMUNITY_CREATE_PATH).toString();
|
return new URLCombiner(getCommunityModuleRoute(), COMMUNITY_CREATE_PATH).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCommunityEditRolesRoute(id) {
|
||||||
|
return new URLCombiner(getCollectionPageRoute(id), COMMUNITY_EDIT_PATH, COMMUNITY_EDIT_ROLES_PATH).toString()
|
||||||
|
}
|
||||||
|
|
||||||
export const COMMUNITY_CREATE_PATH = 'create';
|
export const COMMUNITY_CREATE_PATH = 'create';
|
||||||
export const COMMUNITY_EDIT_PATH = 'edit';
|
export const COMMUNITY_EDIT_PATH = 'edit';
|
||||||
|
export const COMMUNITY_EDIT_ROLES_PATH = 'roles';
|
||||||
|
@@ -2,24 +2,28 @@
|
|||||||
<div class="community-page" *ngIf="communityRD?.hasSucceeded" @fadeInOut>
|
<div class="community-page" *ngIf="communityRD?.hasSucceeded" @fadeInOut>
|
||||||
<div *ngIf="communityRD?.payload; let communityPayload">
|
<div *ngIf="communityRD?.payload; let communityPayload">
|
||||||
<ds-view-tracker [object]="communityPayload"></ds-view-tracker>
|
<ds-view-tracker [object]="communityPayload"></ds-view-tracker>
|
||||||
<header class="comcol-header border-bottom mb-4 pb-4">
|
<div class="d-flex flex-row border-bottom mb-4 pb-4">
|
||||||
<!-- Community name -->
|
<header class="comcol-header mr-auto">
|
||||||
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
|
<!-- Community name -->
|
||||||
<!-- Community logo -->
|
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
|
||||||
<ds-comcol-page-logo *ngIf="logoRD$" [logo]="(logoRD$ | async)?.payload" [alternateText]="'Community Logo'">
|
<!-- Community logo -->
|
||||||
</ds-comcol-page-logo>
|
<ds-comcol-page-logo *ngIf="logoRD$" [logo]="(logoRD$ | async)?.payload" [alternateText]="'Community Logo'">
|
||||||
<!-- Handle -->
|
</ds-comcol-page-logo>
|
||||||
<ds-comcol-page-handle [content]="communityPayload.handle" [title]="'community.page.handle'">
|
<!-- Handle -->
|
||||||
</ds-comcol-page-handle>
|
<ds-comcol-page-handle [content]="communityPayload.handle" [title]="'community.page.handle'">
|
||||||
<!-- Introductory text -->
|
</ds-comcol-page-handle>
|
||||||
<ds-comcol-page-content [content]="communityPayload.introductoryText" [hasInnerHtml]="true">
|
<!-- Introductory text -->
|
||||||
</ds-comcol-page-content>
|
<ds-comcol-page-content [content]="communityPayload.introductoryText" [hasInnerHtml]="true">
|
||||||
<!-- News -->
|
</ds-comcol-page-content>
|
||||||
<ds-comcol-page-content [content]="communityPayload.sidebarText" [hasInnerHtml]="true"
|
<!-- News -->
|
||||||
[title]="'community.page.news'">
|
<ds-comcol-page-content [content]="communityPayload.sidebarText" [hasInnerHtml]="true"
|
||||||
</ds-comcol-page-content>
|
[title]="'community.page.news'">
|
||||||
|
</ds-comcol-page-content>
|
||||||
</header>
|
</header>
|
||||||
|
<div class="pl-2">
|
||||||
|
<ds-dso-page-edit-button [pageRoutePrefix]="'communities'" [dso]="communityPayload" [tooltipMsg]="'community.page.edit'"></ds-dso-page-edit-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<section class="comcol-page-browse-section">
|
<section class="comcol-page-browse-section">
|
||||||
<!-- Browse-By Links -->
|
<!-- Browse-By Links -->
|
||||||
<ds-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type">
|
<ds-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type">
|
||||||
|
@@ -13,7 +13,8 @@ import { MetadataService } from '../core/metadata/metadata.service';
|
|||||||
|
|
||||||
import { fadeInOut } from '../shared/animations/fade';
|
import { fadeInOut } from '../shared/animations/fade';
|
||||||
import { hasValue } from '../shared/empty.util';
|
import { hasValue } from '../shared/empty.util';
|
||||||
import { redirectOn404Or401 } from '../core/shared/operators';
|
import { redirectOn4xx } from '../core/shared/operators';
|
||||||
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-community-page',
|
selector: 'ds-community-page',
|
||||||
@@ -39,7 +40,8 @@ export class CommunityPageComponent implements OnInit {
|
|||||||
private communityDataService: CommunityDataService,
|
private communityDataService: CommunityDataService,
|
||||||
private metadata: MetadataService,
|
private metadata: MetadataService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router
|
private router: Router,
|
||||||
|
private authService: AuthService,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -47,7 +49,7 @@ export class CommunityPageComponent implements OnInit {
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.communityRD$ = this.route.data.pipe(
|
this.communityRD$ = this.route.data.pipe(
|
||||||
map((data) => data.dso as RemoteData<Community>),
|
map((data) => data.dso as RemoteData<Community>),
|
||||||
redirectOn404Or401(this.router)
|
redirectOn4xx(this.router, this.authService)
|
||||||
);
|
);
|
||||||
this.logoRD$ = this.communityRD$.pipe(
|
this.logoRD$ = this.communityRD$.pipe(
|
||||||
map((rd: RemoteData<Community>) => rd.payload),
|
map((rd: RemoteData<Community>) => rd.payload),
|
||||||
|
@@ -12,6 +12,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
|
|||||||
import { CreateCommunityPageComponent } from './create-community-page.component';
|
import { CreateCommunityPageComponent } from './create-community-page.component';
|
||||||
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';
|
||||||
|
import {RequestService} from '../../core/data/request.service';
|
||||||
|
|
||||||
describe('CreateCommunityPageComponent', () => {
|
describe('CreateCommunityPageComponent', () => {
|
||||||
let comp: CreateCommunityPageComponent;
|
let comp: CreateCommunityPageComponent;
|
||||||
@@ -25,7 +26,8 @@ describe('CreateCommunityPageComponent', () => {
|
|||||||
{ provide: CommunityDataService, useValue: { findById: () => observableOf({}) } },
|
{ provide: CommunityDataService, useValue: { findById: () => observableOf({}) } },
|
||||||
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
|
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
|
||||||
{ provide: Router, useValue: {} },
|
{ provide: Router, useValue: {} },
|
||||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||||
|
{ provide: RequestService, useValue: {} }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@@ -6,6 +6,7 @@ import { Router } from '@angular/router';
|
|||||||
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
|
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import {RequestService} from '../../core/data/request.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that represents the page where a user can create a new Community
|
* Component that represents the page where a user can create a new Community
|
||||||
@@ -24,8 +25,9 @@ export class CreateCommunityPageComponent extends CreateComColPageComponent<Comm
|
|||||||
protected routeService: RouteService,
|
protected routeService: RouteService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
protected translate: TranslateService
|
protected translate: TranslateService,
|
||||||
|
protected requestService: RequestService
|
||||||
) {
|
) {
|
||||||
super(communityDataService, communityDataService, routeService, router, notificationsService, translate);
|
super(communityDataService, communityDataService, routeService, router, notificationsService, translate, requestService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
|
|||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import { DeleteCommunityPageComponent } from './delete-community-page.component';
|
import { DeleteCommunityPageComponent } from './delete-community-page.component';
|
||||||
|
import {RequestService} from '../../core/data/request.service';
|
||||||
|
|
||||||
describe('DeleteCommunityPageComponent', () => {
|
describe('DeleteCommunityPageComponent', () => {
|
||||||
let comp: DeleteCommunityPageComponent;
|
let comp: DeleteCommunityPageComponent;
|
||||||
@@ -22,6 +23,7 @@ describe('DeleteCommunityPageComponent', () => {
|
|||||||
{ provide: CommunityDataService, useValue: {} },
|
{ provide: CommunityDataService, useValue: {} },
|
||||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } },
|
||||||
{ provide: NotificationsService, useValue: {} },
|
{ provide: NotificationsService, useValue: {} },
|
||||||
|
{ provide: RequestService, useValue: {}}
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@@ -5,6 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { DeleteComColPageComponent } from '../../shared/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
import { DeleteComColPageComponent } from '../../shared/comcol-forms/delete-comcol-page/delete-comcol-page.component';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import {RequestService} from '../../core/data/request.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that represents the page where a user can delete an existing Community
|
* Component that represents the page where a user can delete an existing Community
|
||||||
@@ -22,8 +23,10 @@ export class DeleteCommunityPageComponent extends DeleteComColPageComponent<Comm
|
|||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected notifications: NotificationsService,
|
protected notifications: NotificationsService,
|
||||||
protected translate: TranslateService
|
protected translate: TranslateService,
|
||||||
|
protected requestService: RequestService
|
||||||
) {
|
) {
|
||||||
super(dsoDataService, router, route, notifications, translate);
|
super(dsoDataService, router, route, notifications, translate, requestService);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -17,7 +17,7 @@ describe('CommunityCurateComponent', () => {
|
|||||||
let dsoNameService;
|
let dsoNameService;
|
||||||
|
|
||||||
const community = Object.assign(new Community(), {
|
const community = Object.assign(new Community(), {
|
||||||
handle: '123456789/1', metadata: {'dc.title': ['Community Name']}
|
metadata: {'dc.title': ['Community Name'], 'dc.identifier.uri': [ { value: '123456789/1'}]}
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
|
@@ -47,7 +47,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
observableCombineLatest(this.route.data, this.route.parent.data).pipe(
|
observableCombineLatest(this.route.data, this.route.parent.data).pipe(
|
||||||
map(([data, parentData]) => Object.assign({}, data, parentData)),
|
map(([data, parentData]) => Object.assign({}, data, parentData)),
|
||||||
map((data) => data.item),
|
map((data) => data.dso),
|
||||||
first(),
|
first(),
|
||||||
map((data: RemoteData<Item>) => data.payload)
|
map((data: RemoteData<Item>) => data.payload)
|
||||||
).subscribe((item: Item) => {
|
).subscribe((item: Item) => {
|
||||||
|
@@ -47,7 +47,7 @@ export class EditItemPageComponent implements OnInit {
|
|||||||
this.pages = this.route.routeConfig.children
|
this.pages = this.route.routeConfig.children
|
||||||
.map((child: any) => child.path)
|
.map((child: any) => child.path)
|
||||||
.filter((path: string) => isNotEmpty(path)); // ignore reroutes
|
.filter((path: string) => isNotEmpty(path)); // ignore reroutes
|
||||||
this.itemRD$ = this.route.data.pipe(map((data) => data.item));
|
this.itemRD$ = this.route.data.pipe(map((data) => data.dso));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -74,7 +74,7 @@ describe('ItemAuthorizationsComponent test suite', () => {
|
|||||||
|
|
||||||
const routeStub = {
|
const routeStub = {
|
||||||
data: observableOf({
|
data: observableOf({
|
||||||
item: createSuccessfulRemoteDataObject(item)
|
dso: createSuccessfulRemoteDataObject(item)
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -75,7 +75,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.item$ = this.route.data.pipe(
|
this.item$ = this.route.data.pipe(
|
||||||
map((data) => data.item),
|
map((data) => data.dso),
|
||||||
getFirstSucceededRemoteDataWithNotEmptyPayload(),
|
getFirstSucceededRemoteDataWithNotEmptyPayload(),
|
||||||
map((item: Item) => this.linkService.resolveLink(
|
map((item: Item) => this.linkService.resolveLink(
|
||||||
item,
|
item,
|
||||||
|
@@ -140,7 +140,7 @@ describe('ItemBitstreamsComponent', () => {
|
|||||||
});
|
});
|
||||||
route = Object.assign({
|
route = Object.assign({
|
||||||
parent: {
|
parent: {
|
||||||
data: observableOf({ item: createMockRD(item) })
|
data: observableOf({ dso: createMockRD(item) })
|
||||||
},
|
},
|
||||||
data: observableOf({}),
|
data: observableOf({}),
|
||||||
url: url
|
url: url
|
||||||
|
@@ -89,7 +89,7 @@ describe('ItemCollectionMapperComponent', () => {
|
|||||||
clearDiscoveryRequests: () => {}
|
clearDiscoveryRequests: () => {}
|
||||||
/* tslint:enable:no-empty */
|
/* tslint:enable:no-empty */
|
||||||
});
|
});
|
||||||
const activatedRouteStub = new ActivatedRouteStub({}, { item: mockItemRD });
|
const activatedRouteStub = new ActivatedRouteStub({}, { dso: mockItemRD });
|
||||||
const translateServiceStub = {
|
const translateServiceStub = {
|
||||||
get: () => of('test-message of item ' + mockItem.name),
|
get: () => of('test-message of item ' + mockItem.name),
|
||||||
onLangChange: new EventEmitter(),
|
onLangChange: new EventEmitter(),
|
||||||
|
@@ -91,7 +91,7 @@ export class ItemCollectionMapperComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.itemRD$ = this.route.data.pipe(map((data) => data.item)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Item>>;
|
this.itemRD$ = this.route.data.pipe(map((data) => data.dso)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Item>>;
|
||||||
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
|
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
|
||||||
this.loadCollectionLists();
|
this.loadCollectionLists();
|
||||||
}
|
}
|
||||||
|
@@ -138,7 +138,7 @@ describe('ItemDeleteComponent', () => {
|
|||||||
|
|
||||||
routeStub = {
|
routeStub = {
|
||||||
data: observableOf({
|
data: observableOf({
|
||||||
item: createSuccessfulRemoteDataObject(mockItem)
|
dso: createSuccessfulRemoteDataObject(mockItem)
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<span>{{metadata?.key?.split('.').join('.​')}}</span>
|
<span>{{metadata?.key?.split('.').join('.​')}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="(editable | async)" class="field-container">
|
<div *ngIf="(editable | async)" class="field-container">
|
||||||
<ds-filter-input-suggestions [suggestions]="(metadataFieldSuggestions | async)"
|
<ds-validation-suggestions [suggestions]="(metadataFieldSuggestions | async)"
|
||||||
[(ngModel)]="metadata.key"
|
[(ngModel)]="metadata.key"
|
||||||
[url]="this.url"
|
[url]="this.url"
|
||||||
[metadata]="this.metadata"
|
[metadata]="this.metadata"
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
[valid]="(valid | async) !== false"
|
[valid]="(valid | async) !== false"
|
||||||
dsAutoFocus autoFocusSelector=".suggestion_input"
|
dsAutoFocus autoFocusSelector=".suggestion_input"
|
||||||
[ngModelOptions]="{standalone: true}"
|
[ngModelOptions]="{standalone: true}"
|
||||||
></ds-filter-input-suggestions>
|
></ds-validation-suggestions>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-danger"
|
<small class="text-danger"
|
||||||
*ngIf="(valid | async) === false">{{"item.edit.metadata.metadatafield.invalid" | translate}}</small>
|
*ngIf="(valid | async) === false">{{"item.edit.metadata.metadatafield.invalid" | translate}}</small>
|
||||||
|
@@ -20,9 +20,9 @@ import {
|
|||||||
} from '../../../../shared/remote-data.utils';
|
} from '../../../../shared/remote-data.utils';
|
||||||
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||||
import { EditInPlaceFieldComponent } from './edit-in-place-field.component';
|
import { EditInPlaceFieldComponent } from './edit-in-place-field.component';
|
||||||
import { FilterInputSuggestionsComponent } from '../../../../shared/input-suggestions/filter-suggestions/filter-input-suggestions.component';
|
|
||||||
import { MockComponent, MockDirective } from 'ng-mocks';
|
import { MockComponent, MockDirective } from 'ng-mocks';
|
||||||
import { DebounceDirective } from '../../../../shared/utils/debounce.directive';
|
import { DebounceDirective } from '../../../../shared/utils/debounce.directive';
|
||||||
|
import { ValidationSuggestionsComponent } from '../../../../shared/input-suggestions/validation-suggestions/validation-suggestions.component';
|
||||||
|
|
||||||
let comp: EditInPlaceFieldComponent;
|
let comp: EditInPlaceFieldComponent;
|
||||||
let fixture: ComponentFixture<EditInPlaceFieldComponent>;
|
let fixture: ComponentFixture<EditInPlaceFieldComponent>;
|
||||||
@@ -88,7 +88,7 @@ describe('EditInPlaceFieldComponent', () => {
|
|||||||
declarations: [
|
declarations: [
|
||||||
EditInPlaceFieldComponent,
|
EditInPlaceFieldComponent,
|
||||||
MockDirective(DebounceDirective),
|
MockDirective(DebounceDirective),
|
||||||
MockComponent(FilterInputSuggestionsComponent)
|
MockComponent(ValidationSuggestionsComponent)
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: metadataFieldService },
|
{ provide: RegistryService, useValue: metadataFieldService },
|
||||||
|
@@ -130,7 +130,7 @@ describe('ItemMetadataComponent', () => {
|
|||||||
routeStub = {
|
routeStub = {
|
||||||
data: observableOf({}),
|
data: observableOf({}),
|
||||||
parent: {
|
parent: {
|
||||||
data: observableOf({ item: createSuccessfulRemoteDataObject(item) })
|
data: observableOf({ dso: createSuccessfulRemoteDataObject(item) })
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
paginatedMetadataFields = new PaginatedList(undefined, [mdField1, mdField2, mdField3]);
|
paginatedMetadataFields = new PaginatedList(undefined, [mdField1, mdField2, mdField3]);
|
||||||
|
@@ -44,7 +44,7 @@ describe('ItemMoveComponent', () => {
|
|||||||
|
|
||||||
const routeStub = {
|
const routeStub = {
|
||||||
data: observableOf({
|
data: observableOf({
|
||||||
item: new RemoteData(false, false, true, null, {
|
dso: new RemoteData(false, false, true, null, {
|
||||||
id: 'item1'
|
id: 'item1'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@@ -55,7 +55,7 @@ export class ItemMoveComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.itemRD$ = this.route.data.pipe(map((data) => data.item), getSucceededRemoteData()) as Observable<RemoteData<Item>>;
|
this.itemRD$ = this.route.data.pipe(map((data) => data.dso), getSucceededRemoteData()) as Observable<RemoteData<Item>>;
|
||||||
this.itemRD$.subscribe((rd) => {
|
this.itemRD$.subscribe((rd) => {
|
||||||
this.itemId = rd.payload.id;
|
this.itemId = rd.payload.id;
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,7 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
|
|||||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -16,8 +17,9 @@ import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
|||||||
export class ItemPageReinstateGuard extends DsoPageFeatureGuard<Item> {
|
export class ItemPageReinstateGuard extends DsoPageFeatureGuard<Item> {
|
||||||
constructor(protected resolver: ItemPageResolver,
|
constructor(protected resolver: ItemPageResolver,
|
||||||
protected authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
protected router: Router) {
|
protected router: Router,
|
||||||
super(resolver, authorizationService, router);
|
protected authService: AuthService) {
|
||||||
|
super(resolver, authorizationService, router, authService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -6,6 +6,7 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
|
|||||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -16,8 +17,9 @@ import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
|||||||
export class ItemPageWithdrawGuard extends DsoPageFeatureGuard<Item> {
|
export class ItemPageWithdrawGuard extends DsoPageFeatureGuard<Item> {
|
||||||
constructor(protected resolver: ItemPageResolver,
|
constructor(protected resolver: ItemPageResolver,
|
||||||
protected authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
protected router: Router) {
|
protected router: Router,
|
||||||
super(resolver, authorizationService, router);
|
protected authService: AuthService) {
|
||||||
|
super(resolver, authorizationService, router, authService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -51,7 +51,7 @@ describe('ItemPrivateComponent', () => {
|
|||||||
|
|
||||||
routeStub = {
|
routeStub = {
|
||||||
data: observableOf({
|
data: observableOf({
|
||||||
item: createSuccessfulRemoteDataObject({
|
dso: createSuccessfulRemoteDataObject({
|
||||||
id: 'fake-id'
|
id: 'fake-id'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@@ -51,7 +51,7 @@ describe('ItemPublicComponent', () => {
|
|||||||
|
|
||||||
routeStub = {
|
routeStub = {
|
||||||
data: observableOf({
|
data: observableOf({
|
||||||
item: createSuccessfulRemoteDataObject({
|
dso: createSuccessfulRemoteDataObject({
|
||||||
id: 'fake-id'
|
id: 'fake-id'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@@ -51,7 +51,7 @@ describe('ItemReinstateComponent', () => {
|
|||||||
|
|
||||||
routeStub = {
|
routeStub = {
|
||||||
data: observableOf({
|
data: observableOf({
|
||||||
item: createSuccessfulRemoteDataObject({
|
dso: createSuccessfulRemoteDataObject({
|
||||||
id: 'fake-id'
|
id: 'fake-id'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@@ -1,15 +1,26 @@
|
|||||||
<h5>{{getRelationshipMessageKey() | async | translate}}</h5>
|
<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 *ngVar="updates$ | async as updates">
|
||||||
<ng-container *ngIf="updates">
|
<ng-container *ngIf="updates">
|
||||||
<ng-container *ngVar="updates | dsObjectValues as updateValues">
|
<ng-container *ngVar="updates | dsObjectValues as updateValues">
|
||||||
<ds-edit-relationship *ngFor="let updateValue of updateValues; trackBy: trackUpdate"
|
<ds-edit-relationship *ngFor="let updateValue of updateValues; trackBy: trackUpdate"
|
||||||
class="relationship-row d-block"
|
class="relationship-row d-block alert"
|
||||||
[fieldUpdate]="updateValue"
|
[fieldUpdate]="updateValue || {}"
|
||||||
[url]="url"
|
[url]="url"
|
||||||
[editItem]="item"
|
[editItem]="item"
|
||||||
[ngClass]="{'alert alert-danger': updateValue?.changeType === 2}">
|
[ngClass]="{
|
||||||
|
'alert-success': updateValue.changeType === 1,
|
||||||
|
'alert-warning': updateValue.changeType === 0,
|
||||||
|
'alert-danger': updateValue.changeType === 2
|
||||||
|
}">
|
||||||
</ds-edit-relationship>
|
</ds-edit-relationship>
|
||||||
|
<div *ngIf="updateValues.length === 0">{{"item.edit.relationships.no-relationships" | translate}}</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div *ngIf="!updates">no relationships</div>
|
<ds-loading *ngIf="!updates"></ds-loading>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
.relationship-row:not(.alert-danger) {
|
.relationship-row:not(.alert) {
|
||||||
padding: $alert-padding-y 0;
|
padding: $alert-padding-y 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.relationship-row.alert-danger {
|
.relationship-row.alert {
|
||||||
margin-left: -$alert-padding-x;
|
margin-left: -$alert-padding-x;
|
||||||
margin-right: -$alert-padding-x;
|
margin-right: -$alert-padding-x;
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
@@ -8,6 +8,7 @@ import { FieldChangeType } from '../../../../core/data/object-updates/object-upd
|
|||||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
import { RelationshipTypeService } from '../../../../core/data/relationship-type.service';
|
import { RelationshipTypeService } from '../../../../core/data/relationship-type.service';
|
||||||
|
import { RelationshipService } from '../../../../core/data/relationship.service';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
||||||
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||||
@@ -15,6 +16,7 @@ import { Relationship } from '../../../../core/shared/item-relationships/relatio
|
|||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||||
import { getMockLinkService } from '../../../../shared/mocks/link-service.mock';
|
import { getMockLinkService } from '../../../../shared/mocks/link-service.mock';
|
||||||
|
import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service';
|
||||||
import { SharedModule } from '../../../../shared/shared.module';
|
import { SharedModule } from '../../../../shared/shared.module';
|
||||||
import { EditRelationshipListComponent } from './edit-relationship-list.component';
|
import { EditRelationshipListComponent } from './edit-relationship-list.component';
|
||||||
|
|
||||||
@@ -22,72 +24,123 @@ let comp: EditRelationshipListComponent;
|
|||||||
let fixture: ComponentFixture<EditRelationshipListComponent>;
|
let fixture: ComponentFixture<EditRelationshipListComponent>;
|
||||||
let de: DebugElement;
|
let de: DebugElement;
|
||||||
|
|
||||||
|
let linkService;
|
||||||
let objectUpdatesService;
|
let objectUpdatesService;
|
||||||
let entityTypeService;
|
let relationshipService;
|
||||||
|
let selectableListService;
|
||||||
|
|
||||||
const url = 'http://test-url.com/test-url';
|
const url = 'http://test-url.com/test-url';
|
||||||
|
|
||||||
let item;
|
let item;
|
||||||
|
let entityType;
|
||||||
|
let relatedEntityType;
|
||||||
let author1;
|
let author1;
|
||||||
let author2;
|
let author2;
|
||||||
let fieldUpdate1;
|
let fieldUpdate1;
|
||||||
let fieldUpdate2;
|
let fieldUpdate2;
|
||||||
let relationship1;
|
let relationships;
|
||||||
let relationship2;
|
|
||||||
let relationshipType;
|
let relationshipType;
|
||||||
let entityType;
|
|
||||||
let relatedEntityType;
|
|
||||||
|
|
||||||
describe('EditRelationshipListComponent', () => {
|
describe('EditRelationshipListComponent', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async(() => {
|
||||||
|
|
||||||
entityType = Object.assign(new ItemType(), {
|
entityType = Object.assign(new ItemType(), {
|
||||||
id: 'entityType',
|
id: 'Publication',
|
||||||
|
uuid: 'Publication',
|
||||||
|
label: 'Publication',
|
||||||
});
|
});
|
||||||
|
|
||||||
relatedEntityType = Object.assign(new ItemType(), {
|
relatedEntityType = Object.assign(new ItemType(), {
|
||||||
id: 'relatedEntityType',
|
id: 'Author',
|
||||||
|
uuid: 'Author',
|
||||||
|
label: 'Author',
|
||||||
});
|
});
|
||||||
|
|
||||||
relationshipType = Object.assign(new RelationshipType(), {
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
id: '1',
|
id: '1',
|
||||||
uuid: '1',
|
uuid: '1',
|
||||||
|
leftType: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
entityType,
|
||||||
|
)),
|
||||||
|
rightType: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
relatedEntityType,
|
||||||
|
)),
|
||||||
leftwardType: 'isAuthorOfPublication',
|
leftwardType: 'isAuthorOfPublication',
|
||||||
rightwardType: 'isPublicationOfAuthor',
|
rightwardType: 'isPublicationOfAuthor',
|
||||||
leftType: observableOf(new RemoteData(false, false, true, undefined, entityType)),
|
|
||||||
rightType: observableOf(new RemoteData(false, false, true, undefined, relatedEntityType)),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
relationship1 = Object.assign(new Relationship(), {
|
author1 = Object.assign(new Item(), {
|
||||||
_links: {
|
id: 'author1',
|
||||||
self: {
|
uuid: 'author1'
|
||||||
href: url + '/2'
|
});
|
||||||
}
|
author2 = Object.assign(new Item(), {
|
||||||
},
|
id: 'author2',
|
||||||
id: '2',
|
uuid: 'author2'
|
||||||
uuid: '2',
|
|
||||||
leftId: 'author1',
|
|
||||||
rightId: 'publication',
|
|
||||||
leftItem: observableOf(new RemoteData(false, false, true, undefined, item)),
|
|
||||||
rightItem: observableOf(new RemoteData(false, false, true, undefined, author1)),
|
|
||||||
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
relationship2 = Object.assign(new Relationship(), {
|
relationships = [
|
||||||
_links: {
|
Object.assign(new Relationship(), {
|
||||||
self: {
|
self: url + '/2',
|
||||||
href: url + '/3'
|
id: '2',
|
||||||
}
|
uuid: '2',
|
||||||
},
|
relationshipType: observableOf(new RemoteData(
|
||||||
id: '3',
|
false,
|
||||||
uuid: '3',
|
false,
|
||||||
leftId: 'author2',
|
true,
|
||||||
rightId: 'publication',
|
undefined,
|
||||||
leftItem: observableOf(new RemoteData(false, false, true, undefined, item)),
|
relationshipType
|
||||||
rightItem: observableOf(new RemoteData(false, false, true, undefined, author2)),
|
)),
|
||||||
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
leftItem: observableOf(new RemoteData(
|
||||||
});
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
item,
|
||||||
|
)),
|
||||||
|
rightItem: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
author1,
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
Object.assign(new Relationship(), {
|
||||||
|
self: url + '/3',
|
||||||
|
id: '3',
|
||||||
|
uuid: '3',
|
||||||
|
relationshipType: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
relationshipType
|
||||||
|
)),
|
||||||
|
leftItem: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
item,
|
||||||
|
)),
|
||||||
|
rightItem: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
author2,
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
item = Object.assign(new Item(), {
|
item = Object.assign(new Item(), {
|
||||||
_links: {
|
_links: {
|
||||||
@@ -100,84 +153,82 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
undefined,
|
undefined,
|
||||||
new PaginatedList(new PageInfo(), [relationship1, relationship2])
|
new PaginatedList(new PageInfo(), relationships),
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
|
|
||||||
author1 = Object.assign(new Item(), {
|
|
||||||
id: 'author1',
|
|
||||||
uuid: 'author1'
|
|
||||||
});
|
|
||||||
author2 = Object.assign(new Item(), {
|
|
||||||
id: 'author2',
|
|
||||||
uuid: 'author2'
|
|
||||||
});
|
|
||||||
|
|
||||||
fieldUpdate1 = {
|
fieldUpdate1 = {
|
||||||
field: author1,
|
field: {
|
||||||
|
uuid: relationships[0].uuid,
|
||||||
|
relationship: relationships[0],
|
||||||
|
type: relationshipType,
|
||||||
|
},
|
||||||
changeType: undefined
|
changeType: undefined
|
||||||
};
|
};
|
||||||
fieldUpdate2 = {
|
fieldUpdate2 = {
|
||||||
field: author2,
|
field: {
|
||||||
|
uuid: relationships[1].uuid,
|
||||||
|
relationship: relationships[1],
|
||||||
|
type: relationshipType,
|
||||||
|
},
|
||||||
changeType: FieldChangeType.REMOVE
|
changeType: FieldChangeType.REMOVE
|
||||||
};
|
};
|
||||||
|
|
||||||
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
||||||
{
|
{
|
||||||
getFieldUpdates: observableOf({
|
getFieldUpdates: observableOf({
|
||||||
[author1.uuid]: fieldUpdate1,
|
[relationships[0].uuid]: fieldUpdate1,
|
||||||
[author2.uuid]: fieldUpdate2
|
[relationships[1].uuid]: fieldUpdate2
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
entityTypeService = jasmine.createSpyObj('entityTypeService',
|
relationshipService = jasmine.createSpyObj('relationshipService',
|
||||||
{
|
{
|
||||||
getEntityTypeByLabel: observableOf(new RemoteData(
|
getRelatedItemsByLabel: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [author1, author2]))),
|
||||||
false,
|
getItemRelationshipsByLabel: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), relationships))),
|
||||||
false,
|
isLeftItem: observableOf(true),
|
||||||
true,
|
|
||||||
null,
|
|
||||||
entityType,
|
|
||||||
)),
|
|
||||||
getEntityTypeRelationships: observableOf(new RemoteData(
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
null,
|
|
||||||
new PaginatedList(new PageInfo(), [relationshipType]),
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
selectableListService = {};
|
||||||
|
|
||||||
|
linkService = {
|
||||||
|
resolveLink: () => null,
|
||||||
|
resolveLinks: () => null,
|
||||||
|
};
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [SharedModule, TranslateModule.forRoot()],
|
imports: [SharedModule, TranslateModule.forRoot()],
|
||||||
declarations: [EditRelationshipListComponent],
|
declarations: [EditRelationshipListComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||||
{ provide: RelationshipTypeService, useValue: {} },
|
{ provide: RelationshipService, useValue: relationshipService },
|
||||||
{ provide: LinkService, useValue: getMockLinkService() },
|
{ provide: SelectableListService, useValue: selectableListService },
|
||||||
|
{ provide: LinkService, useValue: linkService },
|
||||||
], schemas: [
|
], schemas: [
|
||||||
NO_ERRORS_SCHEMA
|
NO_ERRORS_SCHEMA
|
||||||
]
|
]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(EditRelationshipListComponent);
|
fixture = TestBed.createComponent(EditRelationshipListComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
de = fixture.debugElement;
|
de = fixture.debugElement;
|
||||||
|
|
||||||
comp.item = item;
|
comp.item = item;
|
||||||
comp.itemType = entityType;
|
comp.itemType = entityType;
|
||||||
comp.url = url;
|
comp.url = url;
|
||||||
comp.relationshipType = relationshipType;
|
comp.relationshipType = relationshipType;
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('changeType is REMOVE', () => {
|
describe('changeType is REMOVE', () => {
|
||||||
it('the div should have class alert-danger', () => {
|
beforeEach(() => {
|
||||||
|
|
||||||
fieldUpdate1.changeType = FieldChangeType.REMOVE;
|
fieldUpdate1.changeType = FieldChangeType.REMOVE;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('the div should have class alert-danger', () => {
|
||||||
const element = de.queryAll(By.css('.relationship-row'))[1].nativeElement;
|
const element = de.queryAll(By.css('.relationship-row'))[1].nativeElement;
|
||||||
expect(element.classList).toContain('alert-danger');
|
expect(element.classList).toContain('alert-danger');
|
||||||
});
|
});
|
||||||
|
@@ -1,8 +1,11 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
|
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
import { FieldUpdate, FieldUpdates } from '../../../../core/data/object-updates/object-updates.reducer';
|
import { FieldUpdate, FieldUpdates, RelationshipIdentifiable } from '../../../../core/data/object-updates/object-updates.reducer';
|
||||||
|
import { RelationshipService } from '../../../../core/data/relationship.service';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { map, switchMap } from 'rxjs/operators';
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../../../shared/empty.util';
|
import { hasValue } from '../../../../shared/empty.util';
|
||||||
@@ -14,6 +17,11 @@ import {
|
|||||||
getSucceededRemoteData
|
getSucceededRemoteData
|
||||||
} from '../../../../core/shared/operators';
|
} from '../../../../core/shared/operators';
|
||||||
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
||||||
|
import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
||||||
|
import { RelationshipOptions } from '../../../../shared/form/builder/models/relationship-options.model';
|
||||||
|
import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model';
|
||||||
|
import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service';
|
||||||
|
import { SearchResult } from '../../../../shared/search/search-result.model';
|
||||||
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -45,14 +53,29 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() relationshipType: RelationshipType;
|
@Input() relationshipType: RelationshipType;
|
||||||
|
|
||||||
|
private relatedEntityType$: Observable<ItemType>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list ID to save selected entities under
|
||||||
|
*/
|
||||||
|
listId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The FieldUpdates for the relationships in question
|
* The FieldUpdates for the relationships in question
|
||||||
*/
|
*/
|
||||||
updates$: Observable<FieldUpdates>;
|
updates$: Observable<FieldUpdates>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reference to the lookup window
|
||||||
|
*/
|
||||||
|
modalRef: NgbModalRef;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected objectUpdatesService: ObjectUpdatesService,
|
protected objectUpdatesService: ObjectUpdatesService,
|
||||||
protected linkService: LinkService
|
protected linkService: LinkService,
|
||||||
|
protected relationshipService: RelationshipService,
|
||||||
|
protected modalService: NgbModal,
|
||||||
|
protected selectableListService: SelectableListService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,10 +84,18 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
public getRelationshipMessageKey(): Observable<string> {
|
public getRelationshipMessageKey(): Observable<string> {
|
||||||
|
|
||||||
return this.getLabel().pipe(
|
return observableCombineLatest(
|
||||||
map((label) => {
|
this.getLabel(),
|
||||||
if (hasValue(label) && label.indexOf('Of') > -1) {
|
this.relatedEntityType$,
|
||||||
return `relationships.${label.substring(0, label.indexOf('Of') + 2)}`;
|
).pipe(
|
||||||
|
map(([label, relatedEntityType]) => {
|
||||||
|
if (hasValue(label) && label.indexOf('is') > -1 && label.indexOf('Of') > -1) {
|
||||||
|
const relationshipLabel = `${label.substring(2, label.indexOf('Of'))}`;
|
||||||
|
if (relationshipLabel !== relatedEntityType.label) {
|
||||||
|
return `relationships.is${relationshipLabel}Of.${relatedEntityType.label}`
|
||||||
|
} else {
|
||||||
|
return `relationships.is${relationshipLabel}Of`
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
@@ -76,7 +107,6 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
* Get the relevant label for this relationship type
|
* Get the relevant label for this relationship type
|
||||||
*/
|
*/
|
||||||
private getLabel(): Observable<string> {
|
private getLabel(): Observable<string> {
|
||||||
|
|
||||||
return observableCombineLatest([
|
return observableCombineLatest([
|
||||||
this.relationshipType.leftType,
|
this.relationshipType.leftType,
|
||||||
this.relationshipType.rightType,
|
this.relationshipType.rightType,
|
||||||
@@ -98,19 +128,197 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
return update && update.field ? update.field.uuid : undefined;
|
return update && update.field ? update.field.uuid : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the dynamic lookup modal to search for items to add as relationships
|
||||||
|
*/
|
||||||
|
openLookup() {
|
||||||
|
|
||||||
|
this.modalRef = this.modalService.open(DsDynamicLookupRelationModalComponent, {
|
||||||
|
size: 'lg'
|
||||||
|
});
|
||||||
|
const modalComp: DsDynamicLookupRelationModalComponent = this.modalRef.componentInstance;
|
||||||
|
modalComp.repeatable = true;
|
||||||
|
modalComp.listId = this.listId;
|
||||||
|
modalComp.item = this.item;
|
||||||
|
modalComp.select = (...selectableObjects: Array<SearchResult<Item>>) => {
|
||||||
|
selectableObjects.forEach((searchResult) => {
|
||||||
|
const relatedItem: Item = searchResult.indexableObject;
|
||||||
|
this.getFieldUpdatesForRelatedItem(relatedItem)
|
||||||
|
.subscribe((identifiables) => {
|
||||||
|
identifiables.forEach((identifiable) =>
|
||||||
|
this.objectUpdatesService.removeSingleFieldUpdate(this.url, identifiable.uuid)
|
||||||
|
);
|
||||||
|
if (identifiables.length === 0) {
|
||||||
|
this.relationshipService.getNameVariant(this.listId, relatedItem.uuid)
|
||||||
|
.subscribe((nameVariant) => {
|
||||||
|
const update = {
|
||||||
|
uuid: this.relationshipType.id + '-' + relatedItem.uuid,
|
||||||
|
nameVariant,
|
||||||
|
type: this.relationshipType,
|
||||||
|
relatedItem,
|
||||||
|
} as RelationshipIdentifiable;
|
||||||
|
this.objectUpdatesService.saveAddFieldUpdate(this.url, update);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
modalComp.deselect = (...selectableObjects: Array<SearchResult<Item>>) => {
|
||||||
|
selectableObjects.forEach((searchResult) => {
|
||||||
|
const relatedItem: Item = searchResult.indexableObject;
|
||||||
|
this.objectUpdatesService.removeSingleFieldUpdate(this.url, this.relationshipType.id + '-' + relatedItem.uuid);
|
||||||
|
this.getFieldUpdatesForRelatedItem(relatedItem)
|
||||||
|
.subscribe((identifiables) =>
|
||||||
|
identifiables.forEach((identifiable) =>
|
||||||
|
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, identifiable)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
this.relatedEntityType$
|
||||||
|
.pipe(take(1))
|
||||||
|
.subscribe((relatedEntityType) => {
|
||||||
|
modalComp.relationshipOptions = Object.assign(
|
||||||
|
new RelationshipOptions(), {
|
||||||
|
relationshipType: relatedEntityType.label,
|
||||||
|
// filter: this.getRelationshipMessageKey(),
|
||||||
|
searchConfiguration: relatedEntityType.label.toLowerCase(),
|
||||||
|
nameVariants: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.selectableListService.deselectAll(this.listId);
|
||||||
|
this.updates$.pipe(
|
||||||
|
switchMap((updates) =>
|
||||||
|
Object.values(updates).length > 0 ?
|
||||||
|
observableCombineLatest(
|
||||||
|
Object.values(updates)
|
||||||
|
.filter((update) => update.changeType !== FieldChangeType.REMOVE)
|
||||||
|
.map((update) => {
|
||||||
|
const field = update.field as RelationshipIdentifiable;
|
||||||
|
if (field.relationship) {
|
||||||
|
return this.getRelatedItem(field.relationship);
|
||||||
|
} else {
|
||||||
|
return of(field.relatedItem);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
) : of([])
|
||||||
|
),
|
||||||
|
take(1),
|
||||||
|
map((items) => items.map((item) => {
|
||||||
|
const searchResult = new ItemSearchResult();
|
||||||
|
searchResult.indexableObject = item;
|
||||||
|
searchResult.hitHighlights = {};
|
||||||
|
return searchResult;
|
||||||
|
})),
|
||||||
|
).subscribe((items) => {
|
||||||
|
this.selectableListService.select(this.listId, items);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the existing field updates regarding a relationship with a given item
|
||||||
|
* @param relatedItem The item for which to get the existing field updates
|
||||||
|
*/
|
||||||
|
private getFieldUpdatesForRelatedItem(relatedItem: Item): Observable<RelationshipIdentifiable[]> {
|
||||||
|
|
||||||
|
return this.updates$.pipe(
|
||||||
|
take(1),
|
||||||
|
map((updates) => Object.values(updates)
|
||||||
|
.map((update) => update.field as RelationshipIdentifiable)
|
||||||
|
.filter((field) => field.relationship)
|
||||||
|
),
|
||||||
|
flatMap((identifiables) =>
|
||||||
|
observableCombineLatest(
|
||||||
|
identifiables.map((identifiable) => this.getRelatedItem(identifiable.relationship))
|
||||||
|
).pipe(
|
||||||
|
defaultIfEmpty([]),
|
||||||
|
map((relatedItems) =>
|
||||||
|
identifiables.filter((identifiable, index) => relatedItems[index].uuid === relatedItem.uuid)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the related item for a given relationship
|
||||||
|
* @param relationship The relationship for which to get the related item
|
||||||
|
*/
|
||||||
|
private getRelatedItem(relationship: Relationship): Observable<Item> {
|
||||||
|
return this.relationshipService.isLeftItem(relationship, this.item).pipe(
|
||||||
|
switchMap((isLeftItem) => isLeftItem ? relationship.rightItem : relationship.leftItem),
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.updates$ = this.item.relationships.pipe(
|
|
||||||
|
this.relatedEntityType$ =
|
||||||
|
observableCombineLatest([
|
||||||
|
this.relationshipType.leftType,
|
||||||
|
this.relationshipType.rightType,
|
||||||
|
].map((type) => type.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
))).pipe(
|
||||||
|
map((relatedTypes) => relatedTypes.find((relatedType) => relatedType.uuid !== this.itemType.uuid)),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.relatedEntityType$.pipe(
|
||||||
|
take(1)
|
||||||
|
).subscribe(
|
||||||
|
(relatedEntityType) => this.listId = `edit-relationship-${this.itemType.id}-${relatedEntityType.id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
this.updates$ = this.getItemRelationships().pipe(
|
||||||
|
switchMap((relationships) =>
|
||||||
|
observableCombineLatest(
|
||||||
|
relationships.map((relationship) => this.relationshipService.isLeftItem(relationship, this.item))
|
||||||
|
).pipe(
|
||||||
|
defaultIfEmpty([]),
|
||||||
|
map((isLeftItemArray) => isLeftItemArray.map((isLeftItem, index) => {
|
||||||
|
const relationship = relationships[index];
|
||||||
|
const nameVariant = isLeftItem ? relationship.rightwardValue : relationship.leftwardValue;
|
||||||
|
return {
|
||||||
|
uuid: relationship.id,
|
||||||
|
type: this.relationshipType,
|
||||||
|
relationship,
|
||||||
|
nameVariant,
|
||||||
|
} as RelationshipIdentifiable
|
||||||
|
})),
|
||||||
|
)),
|
||||||
|
switchMap((initialFields) => this.objectUpdatesService.getFieldUpdates(this.url, initialFields).pipe(
|
||||||
|
map((fieldUpdates) => {
|
||||||
|
const fieldUpdatesFiltered: FieldUpdates = {};
|
||||||
|
Object.keys(fieldUpdates).forEach((uuid) => {
|
||||||
|
const field = fieldUpdates[uuid].field;
|
||||||
|
if ((field as RelationshipIdentifiable).type.id === this.relationshipType.id) {
|
||||||
|
fieldUpdatesFiltered[uuid] = fieldUpdates[uuid];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return fieldUpdatesFiltered;
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getItemRelationships() {
|
||||||
|
this.linkService.resolveLink(this.item, followLink('relationships'));
|
||||||
|
return this.item.relationships.pipe(
|
||||||
getAllSucceededRemoteData(),
|
getAllSucceededRemoteData(),
|
||||||
map((relationships) => relationships.payload.page.filter((relationship) => relationship)),
|
map((relationships) => relationships.payload.page.filter((relationship) => relationship)),
|
||||||
map((relationships: Relationship[]) =>
|
filter((relationships) => relationships.every((relationship) => !!relationship)),
|
||||||
relationships.map((relationship: Relationship) => {
|
tap((relationships: Relationship[]) =>
|
||||||
|
relationships.forEach((relationship: Relationship) => {
|
||||||
this.linkService.resolveLinks(
|
this.linkService.resolveLinks(
|
||||||
relationship,
|
relationship,
|
||||||
followLink('relationshipType'),
|
followLink('relationshipType'),
|
||||||
followLink('leftItem'),
|
followLink('leftItem'),
|
||||||
followLink('rightItem'),
|
followLink('rightItem'),
|
||||||
);
|
);
|
||||||
return relationship;
|
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
switchMap((itemRelationships: Relationship[]) =>
|
switchMap((itemRelationships: Relationship[]) =>
|
||||||
@@ -121,15 +329,12 @@ export class EditRelationshipListComponent implements OnInit {
|
|||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
))
|
))
|
||||||
).pipe(
|
).pipe(
|
||||||
|
defaultIfEmpty([]),
|
||||||
map((relationshipTypes) => itemRelationships.filter(
|
map((relationshipTypes) => itemRelationships.filter(
|
||||||
(relationship, index) => relationshipTypes[index].id === this.relationshipType.id)
|
(relationship, index) => relationshipTypes[index].id === this.relationshipType.id)
|
||||||
),
|
),
|
||||||
map((relationships) => relationships.map((relationship) =>
|
|
||||||
Object.assign(new Relationship(), relationship, {uuid: relationship.id})
|
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
switchMap((initialFields) => this.objectUpdatesService.getFieldUpdates(this.url, initialFields)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
<div class="row" *ngIf="relatedItem$ | async">
|
<div class="row" *ngIf="relatedItem$ | async">
|
||||||
<div class="col-10 relationship">
|
<div class="col-10 relationship">
|
||||||
<ds-listable-object-component-loader [object]="relatedItem$ | async" [viewMode]="viewMode"></ds-listable-object-component-loader>
|
<ds-listable-object-component-loader
|
||||||
|
[object]="relatedItem$ | async"
|
||||||
|
[viewMode]="viewMode"
|
||||||
|
[value]="nameVariant"
|
||||||
|
>
|
||||||
|
</ds-listable-object-component-loader>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<div class="btn-group relationship-action-buttons">
|
<div class="btn-group relationship-action-buttons">
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { async, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||||
@@ -25,7 +25,7 @@ let fieldUpdate2;
|
|||||||
let relationships;
|
let relationships;
|
||||||
let relationshipType;
|
let relationshipType;
|
||||||
|
|
||||||
let fixture;
|
let fixture: ComponentFixture<EditRelationshipComponent>;
|
||||||
let comp: EditRelationshipComponent;
|
let comp: EditRelationshipComponent;
|
||||||
let de;
|
let de;
|
||||||
let el;
|
let el;
|
||||||
@@ -91,11 +91,17 @@ describe('EditRelationshipComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
fieldUpdate1 = {
|
fieldUpdate1 = {
|
||||||
field: relationships[0],
|
field: {
|
||||||
|
uuid: relationships[0].uuid,
|
||||||
|
relationship: relationships[0],
|
||||||
|
},
|
||||||
changeType: undefined
|
changeType: undefined
|
||||||
};
|
};
|
||||||
fieldUpdate2 = {
|
fieldUpdate2 = {
|
||||||
field: relationships[1],
|
field: {
|
||||||
|
uuid: relationships[1].uuid,
|
||||||
|
relationship: relationships[1],
|
||||||
|
},
|
||||||
changeType: FieldChangeType.REMOVE
|
changeType: FieldChangeType.REMOVE
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,10 +1,13 @@
|
|||||||
import { Component, Input, OnChanges, OnInit } from '@angular/core';
|
import { Component, Input, OnChanges, OnInit } from '@angular/core';
|
||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, of } from 'rxjs';
|
||||||
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
|
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||||
import { DeleteRelationship, FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
|
import {
|
||||||
|
DeleteRelationship,
|
||||||
|
FieldUpdate,
|
||||||
|
RelationshipIdentifiable
|
||||||
|
} from '../../../../core/data/object-updates/object-updates.reducer';
|
||||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||||
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../core/shared/view-mode.model';
|
||||||
@@ -36,8 +39,16 @@ export class EditRelationshipComponent implements OnChanges {
|
|||||||
/**
|
/**
|
||||||
* The relationship being edited
|
* The relationship being edited
|
||||||
*/
|
*/
|
||||||
get relationship(): Relationship {
|
get relationship() {
|
||||||
return this.fieldUpdate.field as Relationship;
|
return this.update.relationship;
|
||||||
|
}
|
||||||
|
|
||||||
|
get update() {
|
||||||
|
return this.fieldUpdate.field as RelationshipIdentifiable;
|
||||||
|
}
|
||||||
|
|
||||||
|
get nameVariant() {
|
||||||
|
return this.update.nameVariant;
|
||||||
}
|
}
|
||||||
|
|
||||||
public leftItem$: Observable<Item>;
|
public leftItem$: Observable<Item>;
|
||||||
@@ -68,24 +79,28 @@ export class EditRelationshipComponent implements OnChanges {
|
|||||||
* Sets the current relationship based on the fieldUpdate input field
|
* Sets the current relationship based on the fieldUpdate input field
|
||||||
*/
|
*/
|
||||||
ngOnChanges(): void {
|
ngOnChanges(): void {
|
||||||
this.leftItem$ = this.relationship.leftItem.pipe(
|
if (this.relationship) {
|
||||||
getSucceededRemoteData(),
|
this.leftItem$ = this.relationship.leftItem.pipe(
|
||||||
getRemoteDataPayload(),
|
getSucceededRemoteData(),
|
||||||
filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid))
|
getRemoteDataPayload(),
|
||||||
);
|
filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid))
|
||||||
this.rightItem$ = this.relationship.rightItem.pipe(
|
);
|
||||||
getSucceededRemoteData(),
|
this.rightItem$ = this.relationship.rightItem.pipe(
|
||||||
getRemoteDataPayload(),
|
getSucceededRemoteData(),
|
||||||
filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid))
|
getRemoteDataPayload(),
|
||||||
);
|
filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid))
|
||||||
this.relatedItem$ = observableCombineLatest(
|
);
|
||||||
this.leftItem$,
|
this.relatedItem$ = observableCombineLatest(
|
||||||
this.rightItem$,
|
this.leftItem$,
|
||||||
).pipe(
|
this.rightItem$,
|
||||||
map((items: Item[]) =>
|
).pipe(
|
||||||
items.find((item) => item.uuid !== this.editItem.uuid)
|
map((items: Item[]) =>
|
||||||
)
|
items.find((item) => item.uuid !== this.editItem.uuid)
|
||||||
);
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.relatedItem$ = of(this.update.relatedItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,7 +151,8 @@ export class EditRelationshipComponent implements OnChanges {
|
|||||||
* Check if a user should be allowed to remove this field
|
* Check if a user should be allowed to remove this field
|
||||||
*/
|
*/
|
||||||
canRemove(): boolean {
|
canRemove(): boolean {
|
||||||
return this.fieldUpdate.changeType !== FieldChangeType.REMOVE;
|
return this.fieldUpdate.changeType !== FieldChangeType.REMOVE
|
||||||
|
&& this.fieldUpdate.changeType !== FieldChangeType.ADD;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -19,14 +19,19 @@
|
|||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div *ngFor="let relationshipType of relationshipTypes$ | async" class="mb-4">
|
<ng-container *ngVar="relationshipTypes$ | async as relationshipTypes">
|
||||||
<ds-edit-relationship-list
|
<ng-container *ngIf="relationshipTypes">
|
||||||
[url]="url"
|
<div *ngFor="let relationshipType of relationshipTypes" class="mb-4">
|
||||||
[item]="item"
|
<ds-edit-relationship-list
|
||||||
[itemType]="entityType"
|
[url]="url"
|
||||||
[relationshipType]="relationshipType"
|
[item]="item"
|
||||||
></ds-edit-relationship-list>
|
[itemType]="entityType$ | async"
|
||||||
</div>
|
[relationshipType]="relationshipType"
|
||||||
|
></ds-edit-relationship-list>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
<ds-loading *ngIf="!relationshipTypes"></ds-loading>
|
||||||
|
</ng-container>
|
||||||
<div class="button-row bottom">
|
<div class="button-row bottom">
|
||||||
<div class="float-right">
|
<div class="float-right">
|
||||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||||
|
@@ -142,7 +142,7 @@ describe('ItemRelationshipsComponent', () => {
|
|||||||
routeStub = {
|
routeStub = {
|
||||||
data: observableOf({}),
|
data: observableOf({}),
|
||||||
parent: {
|
parent: {
|
||||||
data: observableOf({ item: new RemoteData(false, false, true, null, item) })
|
data: observableOf({ dso: new RemoteData(false, false, true, null, item) })
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -3,10 +3,12 @@ import { Item } from '../../../core/shared/item.model';
|
|||||||
import {
|
import {
|
||||||
DeleteRelationship,
|
DeleteRelationship,
|
||||||
FieldUpdate,
|
FieldUpdate,
|
||||||
FieldUpdates
|
FieldUpdates,
|
||||||
|
RelationshipIdentifiable,
|
||||||
} from '../../../core/data/object-updates/object-updates.reducer';
|
} from '../../../core/data/object-updates/object-updates.reducer';
|
||||||
import { Observable, of as observableOf, Subscription, zip as observableZip } from 'rxjs';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
import { filter, map, startWith, switchMap, take} from 'rxjs/operators';
|
||||||
|
import { combineLatest as observableCombineLatest, of as observableOf, zip as observableZip} from 'rxjs';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
@@ -23,7 +25,6 @@ import { RequestService } from '../../../core/data/request.service';
|
|||||||
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
|
||||||
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
|
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
|
||||||
import { EntityTypeService } from '../../../core/data/entity-type.service';
|
import { EntityTypeService } from '../../../core/data/entity-type.service';
|
||||||
import { isNotEmptyOperator } from '../../../shared/empty.util';
|
|
||||||
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions';
|
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions';
|
||||||
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
|
|
||||||
@@ -35,18 +36,18 @@ import { Relationship } from '../../../core/shared/item-relationships/relationsh
|
|||||||
/**
|
/**
|
||||||
* Component for displaying an item's relationships edit page
|
* Component for displaying an item's relationships edit page
|
||||||
*/
|
*/
|
||||||
export class ItemRelationshipsComponent extends AbstractItemUpdateComponent implements OnDestroy {
|
export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
|
||||||
|
|
||||||
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The labels of all different relations within this item
|
* The allowed relationship types for this type of item as an observable list
|
||||||
*/
|
*/
|
||||||
relationshipTypes$: Observable<RelationshipType[]>;
|
relationshipTypes$: Observable<RelationshipType[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A subscription that checks when the item is deleted in cache and reloads the item by sending a new request
|
* The item's entity type as an observable
|
||||||
* This is used to update the item in cache after relationships are deleted
|
|
||||||
*/
|
*/
|
||||||
itemUpdateSubscription: Subscription;
|
|
||||||
entityType$: Observable<ItemType>;
|
entityType$: Observable<ItemType>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -70,15 +71,29 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.itemUpdateSubscription = this.requestService.hasByHrefObservable(this.item.self).pipe(
|
this.initializeItemUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the item (and view) when it's removed in the request cache
|
||||||
|
*/
|
||||||
|
public initializeItemUpdate(): void {
|
||||||
|
this.itemRD$ = this.requestService.hasByHrefObservable(this.item.self).pipe(
|
||||||
filter((exists: boolean) => !exists),
|
filter((exists: boolean) => !exists),
|
||||||
switchMap(() => this.itemService.findById(this.item.uuid,
|
switchMap(() => this.itemService.findById(
|
||||||
|
this.item.uuid,
|
||||||
followLink('owningCollection'),
|
followLink('owningCollection'),
|
||||||
followLink('bundles'),
|
followLink('bundles'),
|
||||||
followLink('relationships'))),
|
followLink('relationships')),
|
||||||
|
),
|
||||||
|
filter((itemRD) => !!itemRD.statusCode),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.itemRD$.pipe(
|
||||||
getSucceededRemoteData(),
|
getSucceededRemoteData(),
|
||||||
).subscribe((itemRD: RemoteData<Item>) => {
|
getRemoteDataPayload(),
|
||||||
this.item = itemRD.payload;
|
).subscribe((item) => {
|
||||||
|
this.item = item;
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
this.initializeUpdates();
|
this.initializeUpdates();
|
||||||
});
|
});
|
||||||
@@ -127,10 +142,12 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
* Make sure the lists are refreshed afterwards and notifications are sent for success and errors
|
* Make sure the lists are refreshed afterwards and notifications are sent for success and errors
|
||||||
*/
|
*/
|
||||||
public submit(): void {
|
public submit(): void {
|
||||||
|
|
||||||
// Get all the relationships that should be removed
|
// Get all the relationships that should be removed
|
||||||
this.relationshipService.getItemRelationshipsArray(this.item).pipe(
|
const removedRelationshipIDs$: Observable<DeleteRelationship[]> = this.relationshipService.getItemRelationshipsArray(this.item).pipe(
|
||||||
|
startWith([]),
|
||||||
map((relationships: Relationship[]) => relationships.map((relationship) =>
|
map((relationships: Relationship[]) => relationships.map((relationship) =>
|
||||||
Object.assign(new Relationship(), relationship, {uuid: relationship.id})
|
Object.assign(new Relationship(), relationship, { uuid: relationship.id })
|
||||||
)),
|
)),
|
||||||
switchMap((relationships: Relationship[]) => {
|
switchMap((relationships: Relationship[]) => {
|
||||||
return this.objectUpdatesService.getFieldUpdatesExclusive(this.url, relationships) as Observable<FieldUpdates>;
|
return this.objectUpdatesService.getFieldUpdatesExclusive(this.url, relationships) as Observable<FieldUpdates>;
|
||||||
@@ -140,31 +157,83 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
.filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE)
|
.filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE)
|
||||||
.map((fieldUpdate: FieldUpdate) => fieldUpdate.field as DeleteRelationship)
|
.map((fieldUpdate: FieldUpdate) => fieldUpdate.field as DeleteRelationship)
|
||||||
),
|
),
|
||||||
isNotEmptyOperator(),
|
);
|
||||||
take(1),
|
|
||||||
switchMap((deleteRelationships: DeleteRelationship[]) =>
|
const addRelatedItems$: Observable<RelationshipIdentifiable[]> = this.objectUpdatesService.getFieldUpdates(this.url, []).pipe(
|
||||||
observableZip(...deleteRelationships.map((deleteRelationship) => {
|
map((fieldUpdates: FieldUpdates) =>
|
||||||
let copyVirtualMetadata: string;
|
Object.values(fieldUpdates)
|
||||||
if (deleteRelationship.keepLeftVirtualMetadata && deleteRelationship.keepRightVirtualMetadata) {
|
.filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.ADD)
|
||||||
copyVirtualMetadata = 'all';
|
.map((fieldUpdate: FieldUpdate) => fieldUpdate.field as RelationshipIdentifiable)
|
||||||
} else if (deleteRelationship.keepLeftVirtualMetadata) {
|
|
||||||
copyVirtualMetadata = 'left';
|
|
||||||
} else if (deleteRelationship.keepRightVirtualMetadata) {
|
|
||||||
copyVirtualMetadata = 'right';
|
|
||||||
} else {
|
|
||||||
copyVirtualMetadata = 'none';
|
|
||||||
}
|
|
||||||
return this.relationshipService.deleteRelationship(deleteRelationship.uuid, copyVirtualMetadata);
|
|
||||||
}
|
|
||||||
))
|
|
||||||
),
|
),
|
||||||
).subscribe((responses: RestResponse[]) => {
|
);
|
||||||
this.itemUpdateSubscription.add(() => {
|
|
||||||
this.displayNotifications(responses);
|
observableCombineLatest(
|
||||||
});
|
removedRelationshipIDs$,
|
||||||
|
addRelatedItems$,
|
||||||
|
).pipe(
|
||||||
|
take(1),
|
||||||
|
).subscribe(([removeRelationshipIDs, addRelatedItems]) => {
|
||||||
|
const actions = [
|
||||||
|
this.deleteRelationships(removeRelationshipIDs),
|
||||||
|
this.addRelationships(addRelatedItems),
|
||||||
|
];
|
||||||
|
actions.forEach((action) =>
|
||||||
|
action.subscribe((response) => {
|
||||||
|
if (response.length > 0) {
|
||||||
|
this.itemRD$.subscribe(() => {
|
||||||
|
this.initializeOriginalFields();
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
this.displayNotifications(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteRelationships(deleteRelationshipIDs: DeleteRelationship[]): Observable<RestResponse[]> {
|
||||||
|
return observableZip(...deleteRelationshipIDs.map((deleteRelationship) => {
|
||||||
|
let copyVirtualMetadata: string;
|
||||||
|
if (deleteRelationship.keepLeftVirtualMetadata && deleteRelationship.keepRightVirtualMetadata) {
|
||||||
|
copyVirtualMetadata = 'all';
|
||||||
|
} else if (deleteRelationship.keepLeftVirtualMetadata) {
|
||||||
|
copyVirtualMetadata = 'left';
|
||||||
|
} else if (deleteRelationship.keepRightVirtualMetadata) {
|
||||||
|
copyVirtualMetadata = 'right';
|
||||||
|
} else {
|
||||||
|
copyVirtualMetadata = 'none';
|
||||||
|
}
|
||||||
|
return this.relationshipService.deleteRelationship(deleteRelationship.uuid, copyVirtualMetadata);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
addRelationships(addRelatedItems: RelationshipIdentifiable[]): Observable<RestResponse[]> {
|
||||||
|
return observableZip(...addRelatedItems.map((addRelationship) =>
|
||||||
|
this.entityType$.pipe(
|
||||||
|
switchMap((entityType) => this.entityTypeService.isLeftType(addRelationship.type, entityType)),
|
||||||
|
switchMap((isLeftType) => {
|
||||||
|
let leftItem: Item;
|
||||||
|
let rightItem: Item;
|
||||||
|
let leftwardValue: string;
|
||||||
|
let rightwardValue: string;
|
||||||
|
if (isLeftType) {
|
||||||
|
leftItem = this.item;
|
||||||
|
rightItem = addRelationship.relatedItem;
|
||||||
|
leftwardValue = null;
|
||||||
|
rightwardValue = addRelationship.nameVariant;
|
||||||
|
} else {
|
||||||
|
leftItem = addRelationship.relatedItem;
|
||||||
|
rightItem = this.item;
|
||||||
|
leftwardValue = addRelationship.nameVariant;
|
||||||
|
rightwardValue = null;
|
||||||
|
}
|
||||||
|
return this.relationshipService.addRelationship(addRelationship.type.id, leftItem, rightItem, leftwardValue, rightwardValue);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display notifications
|
* Display notifications
|
||||||
* - Error notification for each failed response with their message
|
* - Error notification for each failed response with their message
|
||||||
@@ -182,19 +251,14 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved'));
|
this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends all initial values of this item to the object updates service
|
* Sends all initial values of this item to the object updates service
|
||||||
*/
|
*/
|
||||||
public initializeOriginalFields() {
|
public initializeOriginalFields() {
|
||||||
const initialFields = [];
|
return this.relationshipService.getRelatedItems(this.item).pipe(
|
||||||
this.objectUpdatesService.initialize(this.url, initialFields, this.item.lastModified);
|
take(1),
|
||||||
}
|
).subscribe((items: Item[]) => {
|
||||||
|
this.objectUpdatesService.initialize(this.url, items, this.item.lastModified);
|
||||||
/**
|
});
|
||||||
* Unsubscribe from the item update when the component is destroyed
|
|
||||||
*/
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.itemUpdateSubscription.unsubscribe();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -31,7 +31,7 @@ describe('ItemStatusComponent', () => {
|
|||||||
|
|
||||||
const routeStub = {
|
const routeStub = {
|
||||||
parent: {
|
parent: {
|
||||||
data: observableOf({ item: createSuccessfulRemoteDataObject(mockItem) })
|
data: observableOf({ dso: createSuccessfulRemoteDataObject(mockItem) })
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -55,7 +55,7 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.item));
|
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso));
|
||||||
this.itemRD$.pipe(
|
this.itemRD$.pipe(
|
||||||
first(),
|
first(),
|
||||||
map((data: RemoteData<Item>) => data.payload)
|
map((data: RemoteData<Item>) => data.payload)
|
||||||
|
@@ -23,7 +23,7 @@ describe('ItemVersionHistoryComponent', () => {
|
|||||||
declarations: [ItemVersionHistoryComponent, VarDirective],
|
declarations: [ItemVersionHistoryComponent, VarDirective],
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ item: createSuccessfulRemoteDataObject(item) }) } } }
|
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(item) }) } } }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@@ -30,6 +30,6 @@ export class ItemVersionHistoryComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.item)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Item>>;
|
this.itemRD$ = this.route.parent.data.pipe(map((data) => data.dso)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Item>>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -51,7 +51,7 @@ describe('ItemWithdrawComponent', () => {
|
|||||||
|
|
||||||
routeStub = {
|
routeStub = {
|
||||||
data: observableOf({
|
data: observableOf({
|
||||||
item: createSuccessfulRemoteDataObject({
|
dso: createSuccessfulRemoteDataObject({
|
||||||
id: 'fake-id'
|
id: 'fake-id'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@@ -74,7 +74,7 @@ describe('AbstractSimpleItemActionComponent', () => {
|
|||||||
|
|
||||||
routeStub = {
|
routeStub = {
|
||||||
data: observableOf({
|
data: observableOf({
|
||||||
item: createSuccessfulRemoteDataObject({
|
dso: createSuccessfulRemoteDataObject({
|
||||||
id: 'fake-id'
|
id: 'fake-id'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@@ -42,7 +42,7 @@ export class AbstractSimpleItemActionComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.itemRD$ = this.route.data.pipe(
|
this.itemRD$ = this.route.data.pipe(
|
||||||
map((data) => data.item),
|
map((data) => data.dso),
|
||||||
getSucceededRemoteData()
|
getSucceededRemoteData()
|
||||||
)as Observable<RemoteData<Item>>;
|
)as Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
|
@@ -3,7 +3,12 @@
|
|||||||
<div *ngIf="itemRD?.payload as item">
|
<div *ngIf="itemRD?.payload as item">
|
||||||
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
||||||
<ds-view-tracker [object]="item"></ds-view-tracker>
|
<ds-view-tracker [object]="item"></ds-view-tracker>
|
||||||
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
|
<div class="d-flex flex-row">
|
||||||
|
<ds-item-page-title-field class="mr-auto" [item]="item"></ds-item-page-title-field>
|
||||||
|
<div class="pl-2">
|
||||||
|
<ds-dso-page-edit-button [pageRoutePrefix]="'items'" [dso]="item" [tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="simple-view-link my-3">
|
<div class="simple-view-link my-3">
|
||||||
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id]">
|
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id]">
|
||||||
{{"item.page.link.simple" | translate}}
|
{{"item.page.link.simple" | translate}}
|
||||||
|
@@ -13,7 +13,6 @@ import { RouterTestingModule } from '@angular/router/testing';
|
|||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { PageInfo } from '../../core/shared/page-info.model';
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
@@ -21,6 +20,7 @@ import {
|
|||||||
createSuccessfulRemoteDataObject,
|
createSuccessfulRemoteDataObject,
|
||||||
createSuccessfulRemoteDataObject$
|
createSuccessfulRemoteDataObject$
|
||||||
} from '../../shared/remote-data.utils';
|
} from '../../shared/remote-data.utils';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
const mockItem: Item = Object.assign(new Item(), {
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||||
@@ -34,7 +34,7 @@ const mockItem: Item = Object.assign(new Item(), {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const routeStub = Object.assign(new ActivatedRouteStub(), {
|
const routeStub = Object.assign(new ActivatedRouteStub(), {
|
||||||
data: observableOf({ item: createSuccessfulRemoteDataObject(mockItem) })
|
data: observableOf({ dso: createSuccessfulRemoteDataObject(mockItem) })
|
||||||
});
|
});
|
||||||
const metadataServiceStub = {
|
const metadataServiceStub = {
|
||||||
/* tslint:disable:no-empty */
|
/* tslint:disable:no-empty */
|
||||||
@@ -46,7 +46,14 @@ describe('FullItemPageComponent', () => {
|
|||||||
let comp: FullItemPageComponent;
|
let comp: FullItemPageComponent;
|
||||||
let fixture: ComponentFixture<FullItemPageComponent>;
|
let fixture: ComponentFixture<FullItemPageComponent>;
|
||||||
|
|
||||||
|
let authService: AuthService;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true),
|
||||||
|
setRedirectUrl: {}
|
||||||
|
});
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot({
|
imports: [TranslateModule.forRoot({
|
||||||
loader: {
|
loader: {
|
||||||
@@ -58,7 +65,8 @@ describe('FullItemPageComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{provide: ActivatedRoute, useValue: routeStub},
|
{provide: ActivatedRoute, useValue: routeStub},
|
||||||
{provide: ItemDataService, useValue: {}},
|
{provide: ItemDataService, useValue: {}},
|
||||||
{provide: MetadataService, useValue: metadataServiceStub}
|
{provide: MetadataService, useValue: metadataServiceStub},
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
],
|
],
|
||||||
|
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
@@ -15,6 +15,7 @@ import { MetadataService } from '../../core/metadata/metadata.service';
|
|||||||
|
|
||||||
import { fadeInOut } from '../../shared/animations/fade';
|
import { fadeInOut } from '../../shared/animations/fade';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a simple item page.
|
* This component renders a simple item page.
|
||||||
@@ -35,8 +36,8 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit {
|
|||||||
|
|
||||||
metadata$: Observable<MetadataMap>;
|
metadata$: Observable<MetadataMap>;
|
||||||
|
|
||||||
constructor(route: ActivatedRoute, router: Router, items: ItemDataService, metadataService: MetadataService) {
|
constructor(route: ActivatedRoute, router: Router, items: ItemDataService, metadataService: MetadataService, authService: AuthService) {
|
||||||
super(route, router, items, metadataService);
|
super(route, router, items, metadataService, authService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** AoT inheritance fix, will hopefully be resolved in the near future **/
|
/*** AoT inheritance fix, will hopefully be resolved in the near future **/
|
||||||
|
@@ -6,6 +6,8 @@ import { Item } from '../core/shared/item.model';
|
|||||||
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -16,8 +18,9 @@ import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
|||||||
export class ItemPageAdministratorGuard extends DsoPageFeatureGuard<Item> {
|
export class ItemPageAdministratorGuard extends DsoPageFeatureGuard<Item> {
|
||||||
constructor(protected resolver: ItemPageResolver,
|
constructor(protected resolver: ItemPageResolver,
|
||||||
protected authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
protected router: Router) {
|
protected router: Router,
|
||||||
super(resolver, authorizationService, router);
|
protected authService: AuthService) {
|
||||||
|
super(resolver, authorizationService, router, authService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -20,7 +20,7 @@ import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
|
|||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
resolve: {
|
resolve: {
|
||||||
item: ItemPageResolver,
|
dso: ItemPageResolver,
|
||||||
breadcrumb: ItemBreadcrumbResolver
|
breadcrumb: ItemBreadcrumbResolver
|
||||||
},
|
},
|
||||||
runGuardsAndResolvers: 'always',
|
runGuardsAndResolvers: 'always',
|
||||||
|
@@ -8,7 +8,6 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||||
import { MetadataService } from '../../core/metadata/metadata.service';
|
import { MetadataService } from '../../core/metadata/metadata.service';
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
import { PageInfo } from '../../core/shared/page-info.model';
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
@@ -20,6 +19,7 @@ import {
|
|||||||
createFailedRemoteDataObject$, createPendingRemoteDataObject$, createSuccessfulRemoteDataObject,
|
createFailedRemoteDataObject$, createPendingRemoteDataObject$, createSuccessfulRemoteDataObject,
|
||||||
createSuccessfulRemoteDataObject$
|
createSuccessfulRemoteDataObject$
|
||||||
} from '../../shared/remote-data.utils';
|
} from '../../shared/remote-data.utils';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
const mockItem: Item = Object.assign(new Item(), {
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
|
||||||
@@ -30,6 +30,7 @@ const mockItem: Item = Object.assign(new Item(), {
|
|||||||
describe('ItemPageComponent', () => {
|
describe('ItemPageComponent', () => {
|
||||||
let comp: ItemPageComponent;
|
let comp: ItemPageComponent;
|
||||||
let fixture: ComponentFixture<ItemPageComponent>;
|
let fixture: ComponentFixture<ItemPageComponent>;
|
||||||
|
let authService: AuthService;
|
||||||
|
|
||||||
const mockMetadataService = {
|
const mockMetadataService = {
|
||||||
/* tslint:disable:no-empty */
|
/* tslint:disable:no-empty */
|
||||||
@@ -37,10 +38,15 @@ describe('ItemPageComponent', () => {
|
|||||||
/* tslint:enable:no-empty */
|
/* tslint:enable:no-empty */
|
||||||
};
|
};
|
||||||
const mockRoute = Object.assign(new ActivatedRouteStub(), {
|
const mockRoute = Object.assign(new ActivatedRouteStub(), {
|
||||||
data: observableOf({ item: createSuccessfulRemoteDataObject(mockItem) })
|
data: observableOf({ dso: createSuccessfulRemoteDataObject(mockItem) })
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true),
|
||||||
|
setRedirectUrl: {}
|
||||||
|
});
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot({
|
imports: [TranslateModule.forRoot({
|
||||||
loader: {
|
loader: {
|
||||||
@@ -53,7 +59,8 @@ describe('ItemPageComponent', () => {
|
|||||||
{provide: ActivatedRoute, useValue: mockRoute},
|
{provide: ActivatedRoute, useValue: mockRoute},
|
||||||
{provide: ItemDataService, useValue: {}},
|
{provide: ItemDataService, useValue: {}},
|
||||||
{provide: MetadataService, useValue: mockMetadataService},
|
{provide: MetadataService, useValue: mockMetadataService},
|
||||||
{provide: Router, useValue: {}}
|
{provide: Router, useValue: {}},
|
||||||
|
{ provide: AuthService, useValue: authService },
|
||||||
],
|
],
|
||||||
|
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
@@ -11,8 +11,9 @@ import { Item } from '../../core/shared/item.model';
|
|||||||
import { MetadataService } from '../../core/metadata/metadata.service';
|
import { MetadataService } from '../../core/metadata/metadata.service';
|
||||||
|
|
||||||
import { fadeInOut } from '../../shared/animations/fade';
|
import { fadeInOut } from '../../shared/animations/fade';
|
||||||
import { redirectOn404Or401 } from '../../core/shared/operators';
|
import { redirectOn4xx } from '../../core/shared/operators';
|
||||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a simple item page.
|
* This component renders a simple item page.
|
||||||
@@ -48,6 +49,7 @@ export class ItemPageComponent implements OnInit {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private items: ItemDataService,
|
private items: ItemDataService,
|
||||||
private metadataService: MetadataService,
|
private metadataService: MetadataService,
|
||||||
|
private authService: AuthService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,8 +57,8 @@ export class ItemPageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.itemRD$ = this.route.data.pipe(
|
this.itemRD$ = this.route.data.pipe(
|
||||||
map((data) => data.item as RemoteData<Item>),
|
map((data) => data.dso as RemoteData<Item>),
|
||||||
redirectOn404Or401(this.router)
|
redirectOn4xx(this.router, this.authService)
|
||||||
);
|
);
|
||||||
this.metadataService.processRemoteData(this.itemRD$);
|
this.metadataService.processRemoteData(this.itemRD$);
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
<h2 class="item-page-title-field">
|
<div class="d-flex flex-row">
|
||||||
{{'publication.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
<h2 class="item-page-title-field mr-auto">
|
||||||
</h2>
|
{{'publication.page.titleprefix' | translate}}<ds-metadata-values [mdValues]="object?.allMetadata(['dc.title'])"></ds-metadata-values>
|
||||||
|
</h2>
|
||||||
|
<div class="pl-2">
|
||||||
|
<ds-dso-page-edit-button [pageRoutePrefix]="'items'" [dso]="object" [tooltipMsg]="'publication.page.edit'"></ds-dso-page-edit-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4">
|
<div class="col-xs-12 col-md-4">
|
||||||
<ds-metadata-field-wrapper>
|
<ds-metadata-field-wrapper>
|
||||||
|
@@ -55,10 +55,10 @@ export function getDSORoute(dso: DSpaceObject): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UNAUTHORIZED_PATH = '401';
|
export const FORBIDDEN_PATH = '403';
|
||||||
|
|
||||||
export function getUnauthorizedRoute() {
|
export function getForbiddenRoute() {
|
||||||
return `/${UNAUTHORIZED_PATH}`;
|
return `/${FORBIDDEN_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PAGE_NOT_FOUND_PATH = '404';
|
export const PAGE_NOT_FOUND_PATH = '404';
|
||||||
|
@@ -5,15 +5,14 @@ import { AuthBlockingGuard } from './core/auth/auth-blocking.guard';
|
|||||||
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
||||||
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
|
import { AuthenticatedGuard } from './core/auth/authenticated.guard';
|
||||||
import { SiteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
import { SiteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||||
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
|
|
||||||
import {
|
import {
|
||||||
ADMIN_MODULE_PATH,
|
ADMIN_MODULE_PATH,
|
||||||
BITSTREAM_MODULE_PATH,
|
BITSTREAM_MODULE_PATH,
|
||||||
|
FORBIDDEN_PATH
|
||||||
FORGOT_PASSWORD_PATH,
|
FORGOT_PASSWORD_PATH,
|
||||||
INFO_MODULE_PATH,
|
INFO_MODULE_PATH,
|
||||||
PROFILE_MODULE_PATH,
|
PROFILE_MODULE_PATH,
|
||||||
REGISTER_PATH,
|
REGISTER_PATH,
|
||||||
UNAUTHORIZED_PATH,
|
|
||||||
WORKFLOW_ITEM_MODULE_PATH
|
WORKFLOW_ITEM_MODULE_PATH
|
||||||
} from './app-routing-paths';
|
} from './app-routing-paths';
|
||||||
import { COLLECTION_MODULE_PATH } from './+collection-page/collection-page-routing-paths';
|
import { COLLECTION_MODULE_PATH } from './+collection-page/collection-page-routing-paths';
|
||||||
@@ -22,6 +21,7 @@ import { ITEM_MODULE_PATH } from './+item-page/item-page-routing-paths';
|
|||||||
import { ReloadGuard } from './core/reload/reload.guard';
|
import { ReloadGuard } from './core/reload/reload.guard';
|
||||||
import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.guard';
|
import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.guard';
|
||||||
import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard';
|
import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard';
|
||||||
|
import { ForbiddenComponent } from './forbidden/forbidden.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -166,8 +166,8 @@ import { SiteRegisterGuard } from './core/data/feature-authorization/feature-aut
|
|||||||
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule),
|
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: UNAUTHORIZED_PATH,
|
path: FORBIDDEN_PATH,
|
||||||
component: UnauthorizedComponent
|
component: ForbiddenComponent
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'statistics',
|
path: 'statistics',
|
||||||
|
@@ -41,7 +41,7 @@ import { SharedModule } from './shared/shared.module';
|
|||||||
import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component';
|
import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component';
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
|
import { ForbiddenComponent } from './forbidden/forbidden.component';
|
||||||
|
|
||||||
export function getBase() {
|
export function getBase() {
|
||||||
return environment.ui.nameSpace;
|
return environment.ui.nameSpace;
|
||||||
@@ -116,6 +116,8 @@ const DECLARATIONS = [
|
|||||||
NotificationComponent,
|
NotificationComponent,
|
||||||
NotificationsBoardComponent,
|
NotificationsBoardComponent,
|
||||||
SearchNavbarComponent,
|
SearchNavbarComponent,
|
||||||
|
BreadcrumbsComponent,
|
||||||
|
ForbiddenComponent,
|
||||||
];
|
];
|
||||||
|
|
||||||
const EXPORTS = [
|
const EXPORTS = [
|
||||||
@@ -133,8 +135,6 @@ const EXPORTS = [
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
...DECLARATIONS,
|
...DECLARATIONS,
|
||||||
BreadcrumbsComponent,
|
|
||||||
UnauthorizedComponent,
|
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
...EXPORTS
|
...EXPORTS
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<nav *ngIf="showBreadcrumbs" aria-label="breadcrumb">
|
<nav *ngIf="showBreadcrumbs" aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngTemplateOutlet="breadcrumbs?.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'Home', url: '/'}"></ng-container>
|
*ngTemplateOutlet="breadcrumbs?.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'home.breadcrumbs', url: '/'}"></ng-container>
|
||||||
<ng-container *ngFor="let bc of breadcrumbs; let last = last;">
|
<ng-container *ngFor="let bc of breadcrumbs; let last = last;">
|
||||||
<ng-container *ngTemplateOutlet="!last ? breadcrumb : activeBreadcrumb; context: bc"></ng-container>
|
<ng-container *ngTemplateOutlet="!last ? breadcrumb : activeBreadcrumb; context: bc"></ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { NgZone } from '@angular/core';
|
import { Subscription } from 'rxjs/internal/Subscription';
|
||||||
import { FindListOptions } from '../core/data/request.models';
|
import { FindListOptions } from '../core/data/request.models';
|
||||||
|
import { hasValue } from '../shared/empty.util';
|
||||||
import { CommunityListService, FlatNode } from './community-list-service';
|
import { CommunityListService, FlatNode } from './community-list-service';
|
||||||
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
|
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
|
||||||
import { BehaviorSubject, Observable, } from 'rxjs';
|
import { BehaviorSubject, Observable, } from 'rxjs';
|
||||||
import { finalize, take, } from 'rxjs/operators';
|
import { finalize } from 'rxjs/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DataSource object needed by a CDK Tree to render its nodes.
|
* DataSource object needed by a CDK Tree to render its nodes.
|
||||||
@@ -15,9 +16,9 @@ export class CommunityListDatasource implements DataSource<FlatNode> {
|
|||||||
|
|
||||||
private communityList$ = new BehaviorSubject<FlatNode[]>([]);
|
private communityList$ = new BehaviorSubject<FlatNode[]>([]);
|
||||||
public loading$ = new BehaviorSubject<boolean>(false);
|
public loading$ = new BehaviorSubject<boolean>(false);
|
||||||
|
private subLoadCommunities: Subscription;
|
||||||
|
|
||||||
constructor(private communityListService: CommunityListService,
|
constructor(private communityListService: CommunityListService) {
|
||||||
private zone: NgZone) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(collectionViewer: CollectionViewer): Observable<FlatNode[]> {
|
connect(collectionViewer: CollectionViewer): Observable<FlatNode[]> {
|
||||||
@@ -26,13 +27,13 @@ export class CommunityListDatasource implements DataSource<FlatNode> {
|
|||||||
|
|
||||||
loadCommunities(findOptions: FindListOptions, expandedNodes: FlatNode[]) {
|
loadCommunities(findOptions: FindListOptions, expandedNodes: FlatNode[]) {
|
||||||
this.loading$.next(true);
|
this.loading$.next(true);
|
||||||
this.zone.runOutsideAngular(() => {
|
if (hasValue(this.subLoadCommunities)) {
|
||||||
this.communityListService.loadCommunities(findOptions, expandedNodes).pipe(
|
this.subLoadCommunities.unsubscribe();
|
||||||
take(1),
|
}
|
||||||
finalize(() => this.zone.run(() => this.loading$.next(false))),
|
this.subLoadCommunities = this.communityListService.loadCommunities(findOptions, expandedNodes).pipe(
|
||||||
).subscribe((flatNodes: FlatNode[]) => {
|
finalize(() => this.loading$.next(false)),
|
||||||
this.zone.run(() => this.communityList$.next(flatNodes));
|
).subscribe((flatNodes: FlatNode[]) => {
|
||||||
});
|
this.communityList$.next(flatNodes);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { createSelector, Store } from '@ngrx/store';
|
import { createSelector, Store } from '@ngrx/store';
|
||||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
import { AppState } from '../app.reducer';
|
import { AppState } from '../app.reducer';
|
||||||
import { CommunityDataService } from '../core/data/community-data.service';
|
import { CommunityDataService } from '../core/data/community-data.service';
|
||||||
import { FindListOptions } from '../core/data/request.models';
|
import { FindListOptions } from '../core/data/request.models';
|
||||||
import { map, mergeMap } from 'rxjs/operators';
|
|
||||||
import { Community } from '../core/shared/community.model';
|
import { Community } from '../core/shared/community.model';
|
||||||
import { Collection } from '../core/shared/collection.model';
|
import { Collection } from '../core/shared/collection.model';
|
||||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
|
||||||
import { PageInfo } from '../core/shared/page-info.model';
|
import { PageInfo } from '../core/shared/page-info.model';
|
||||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
@@ -148,8 +148,8 @@ export class CommunityListService {
|
|||||||
return new PaginatedList(newPageInfo, newPage);
|
return new PaginatedList(newPageInfo, newPage);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return topComs$.pipe(mergeMap((topComs: PaginatedList<Community>) => this.transformListOfCommunities(topComs, 0, null, expandedNodes)));
|
return topComs$.pipe(switchMap((topComs: PaginatedList<Community>) => this.transformListOfCommunities(topComs, 0, null, expandedNodes)));
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Puts the initial top level communities in a list to be called upon
|
* Puts the initial top level communities in a list to be called upon
|
||||||
@@ -228,9 +228,13 @@ export class CommunityListService {
|
|||||||
currentPage: i
|
currentPage: i
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
getSucceededRemoteData(),
|
switchMap((rd: RemoteData<PaginatedList<Community>>) => {
|
||||||
mergeMap((rd: RemoteData<PaginatedList<Community>>) =>
|
if (hasValue(rd) && hasValue(rd.payload)) {
|
||||||
this.transformListOfCommunities(rd.payload, level + 1, communityFlatNode, expandedNodes))
|
return this.transformListOfCommunities(rd.payload, level + 1, communityFlatNode, expandedNodes);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
subcoms = [...subcoms, nextSetOfSubcommunitiesPage];
|
subcoms = [...subcoms, nextSetOfSubcommunitiesPage];
|
||||||
@@ -246,14 +250,17 @@ export class CommunityListService {
|
|||||||
currentPage: i
|
currentPage: i
|
||||||
})
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
getSucceededRemoteData(),
|
|
||||||
map((rd: RemoteData<PaginatedList<Collection>>) => {
|
map((rd: RemoteData<PaginatedList<Collection>>) => {
|
||||||
let nodes = rd.payload.page
|
if (hasValue(rd) && hasValue(rd.payload)) {
|
||||||
.map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode));
|
let nodes = rd.payload.page
|
||||||
if (currentCollectionPage < rd.payload.totalPages && currentCollectionPage === rd.payload.currentPage) {
|
.map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode));
|
||||||
nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)];
|
if (currentCollectionPage < rd.payload.totalPages && currentCollectionPage === rd.payload.currentPage) {
|
||||||
|
nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)];
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
return nodes;
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
collections = [...collections, nextSetOfCollectionsPage];
|
collections = [...collections, nextSetOfCollectionsPage];
|
||||||
@@ -275,14 +282,24 @@ export class CommunityListService {
|
|||||||
let hasColls$: Observable<boolean>;
|
let hasColls$: Observable<boolean>;
|
||||||
hasSubcoms$ = this.communityDataService.findByParent(community.uuid, { elementsPerPage: 1 })
|
hasSubcoms$ = this.communityDataService.findByParent(community.uuid, { elementsPerPage: 1 })
|
||||||
.pipe(
|
.pipe(
|
||||||
getSucceededRemoteData(),
|
map((rd: RemoteData<PaginatedList<Community>>) => {
|
||||||
map((results) => results.payload.totalElements > 0),
|
if (hasValue(rd) && hasValue(rd.payload)) {
|
||||||
|
return rd.payload.totalElements > 0;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 })
|
hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 })
|
||||||
.pipe(
|
.pipe(
|
||||||
getSucceededRemoteData(),
|
map((rd: RemoteData<PaginatedList<Collection>>) => {
|
||||||
map((results) => results.payload.totalElements > 0),
|
if (hasValue(rd) && hasValue(rd.payload)) {
|
||||||
|
return rd.payload.totalElements > 0;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let hasChildren$: Observable<boolean>;
|
let hasChildren$: Observable<boolean>;
|
||||||
|
@@ -28,7 +28,7 @@
|
|||||||
<button type="button" class="btn btn-default" cdkTreeNodeToggle
|
<button type="button" class="btn btn-default" cdkTreeNodeToggle
|
||||||
[attr.aria-label]="'toggle ' + node.name"
|
[attr.aria-label]="'toggle ' + node.name"
|
||||||
(click)="toggleExpanded(node)"
|
(click)="toggleExpanded(node)"
|
||||||
[ngClass]="(node.isExpandable$ | async) ? 'visible' : 'invisible'">
|
[ngClass]="(hasChild(null, node)| async) ? 'visible' : 'invisible'">
|
||||||
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
|
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
|
||||||
aria-hidden="true"></span>
|
aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, NgZone, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
import { FindListOptions } from '../../core/data/request.models';
|
import { FindListOptions } from '../../core/data/request.models';
|
||||||
@@ -24,15 +24,14 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
|||||||
public loadingNode: FlatNode;
|
public loadingNode: FlatNode;
|
||||||
|
|
||||||
treeControl = new FlatTreeControl<FlatNode>(
|
treeControl = new FlatTreeControl<FlatNode>(
|
||||||
(node) => node.level, (node) => true
|
(node: FlatNode) => node.level, (node: FlatNode) => true
|
||||||
);
|
);
|
||||||
|
|
||||||
dataSource: CommunityListDatasource;
|
dataSource: CommunityListDatasource;
|
||||||
|
|
||||||
paginationConfig: FindListOptions;
|
paginationConfig: FindListOptions;
|
||||||
|
|
||||||
constructor(private communityListService: CommunityListService,
|
constructor(private communityListService: CommunityListService) {
|
||||||
private zone: NgZone) {
|
|
||||||
this.paginationConfig = new FindListOptions();
|
this.paginationConfig = new FindListOptions();
|
||||||
this.paginationConfig.elementsPerPage = 2;
|
this.paginationConfig.elementsPerPage = 2;
|
||||||
this.paginationConfig.currentPage = 1;
|
this.paginationConfig.currentPage = 1;
|
||||||
@@ -40,7 +39,7 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.dataSource = new CommunityListDatasource(this.communityListService, this.zone);
|
this.dataSource = new CommunityListDatasource(this.communityListService);
|
||||||
this.communityListService.getLoadingNodeFromStore().pipe(take(1)).subscribe((result) => {
|
this.communityListService.getLoadingNodeFromStore().pipe(take(1)).subscribe((result) => {
|
||||||
this.loadingNode = result;
|
this.loadingNode = result;
|
||||||
});
|
});
|
||||||
@@ -65,7 +64,7 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles the expanded variable of a node, adds it to the exapanded nodes list and reloads the tree so this node is expanded
|
* Toggles the expanded variable of a node, adds it to the expanded nodes list and reloads the tree so this node is expanded
|
||||||
* @param node Node we want to expand
|
* @param node Node we want to expand
|
||||||
*/
|
*/
|
||||||
toggleExpanded(node: FlatNode) {
|
toggleExpanded(node: FlatNode) {
|
||||||
|
@@ -453,7 +453,7 @@ export class AuthService {
|
|||||||
* Clear redirect url
|
* Clear redirect url
|
||||||
*/
|
*/
|
||||||
clearRedirectUrl() {
|
clearRedirectUrl() {
|
||||||
this.store.dispatch(new SetRedirectUrlAction(''));
|
this.store.dispatch(new SetRedirectUrlAction(undefined));
|
||||||
this.storage.remove(REDIRECT_COOKIE);
|
this.storage.remove(REDIRECT_COOKIE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,7 +6,6 @@ import { HALLink } from '../../shared/hal-link.model';
|
|||||||
import { HALResource } from '../../shared/hal-resource.model';
|
import { HALResource } from '../../shared/hal-resource.model';
|
||||||
import { ResourceType } from '../../shared/resource-type';
|
import { ResourceType } from '../../shared/resource-type';
|
||||||
import * as decorators from './build-decorators';
|
import * as decorators from './build-decorators';
|
||||||
import { getDataServiceFor } from './build-decorators';
|
|
||||||
import { LinkService } from './link.service';
|
import { LinkService } from './link.service';
|
||||||
|
|
||||||
const spyOnFunction = <T>(obj: T, func: keyof T) => {
|
const spyOnFunction = <T>(obj: T, func: keyof T) => {
|
||||||
|
2
src/app/core/cache/builders/link.service.ts
vendored
2
src/app/core/cache/builders/link.service.ts
vendored
@@ -27,7 +27,7 @@ export class LinkService {
|
|||||||
*/
|
*/
|
||||||
public resolveLinks<T extends HALResource>(model: T, ...linksToFollow: FollowLinkConfig<T>[]): T {
|
public resolveLinks<T extends HALResource>(model: T, ...linksToFollow: FollowLinkConfig<T>[]): T {
|
||||||
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
|
linksToFollow.forEach((linkToFollow: FollowLinkConfig<T>) => {
|
||||||
this.resolveLink(model, linkToFollow);
|
this.resolveLink(model, linkToFollow);
|
||||||
});
|
});
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
@@ -17,6 +17,7 @@ import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
|||||||
import { FindByIDRequest, FindListOptions } from './request.models';
|
import { FindByIDRequest, FindListOptions } from './request.models';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
|
import {createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$} from '../../shared/remote-data.utils';
|
||||||
|
|
||||||
const LINK_NAME = 'test';
|
const LINK_NAME = 'test';
|
||||||
|
|
||||||
@@ -51,7 +52,9 @@ describe('ComColDataService', () => {
|
|||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheService;
|
||||||
let halService: any = {};
|
let halService: any = {};
|
||||||
|
|
||||||
const rdbService = {} as RemoteDataBuildService;
|
const rdbService = {
|
||||||
|
buildSingle : () => null
|
||||||
|
} as any;
|
||||||
const store = {} as Store<CoreState>;
|
const store = {} as Store<CoreState>;
|
||||||
const notificationsService = {} as NotificationsService;
|
const notificationsService = {} as NotificationsService;
|
||||||
const http = {} as HttpClient;
|
const http = {} as HttpClient;
|
||||||
@@ -178,6 +181,90 @@ describe('ComColDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('cache refresh', () => {
|
||||||
|
let communityWithoutParentHref;
|
||||||
|
let data;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
halService = {
|
||||||
|
getEndpoint: (linkPath) => 'https://rest.api/core/' + linkPath
|
||||||
|
};
|
||||||
|
service = initTestService();
|
||||||
|
|
||||||
|
})
|
||||||
|
describe('cache refreshed top level community', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(rdbService, 'buildSingle').and.returnValue(createNoContentRemoteDataObject$());
|
||||||
|
data = {
|
||||||
|
dso: Object.assign(new Community(), {
|
||||||
|
metadata: [{
|
||||||
|
key: 'dc.title',
|
||||||
|
value: 'top level community'
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
_links: {
|
||||||
|
parentCommunity: {
|
||||||
|
href: 'topLevel/parentCommunity'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
communityWithoutParentHref = {
|
||||||
|
dso: Object.assign(new Community(), {
|
||||||
|
metadata: [{
|
||||||
|
key: 'dc.title',
|
||||||
|
value: 'top level community'
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
_links: {}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
it('top level community cache refreshed', () => {
|
||||||
|
scheduler.schedule(() => (service as any).refreshCache(data));
|
||||||
|
scheduler.flush();
|
||||||
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith('https://rest.api/core/communities/search/top');
|
||||||
|
});
|
||||||
|
it('top level community without parent link, cache not refreshed', () => {
|
||||||
|
scheduler.schedule(() => (service as any).refreshCache(communityWithoutParentHref));
|
||||||
|
scheduler.flush();
|
||||||
|
expect(requestService.removeByHrefSubstring).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cache refreshed child community', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const parentCommunity = Object.assign(new Community(), {
|
||||||
|
uuid: 'a20da287-e174-466a-9926-f66as300d399',
|
||||||
|
id: 'a20da287-e174-466a-9926-f66as300d399',
|
||||||
|
metadata: [{
|
||||||
|
key: 'dc.title',
|
||||||
|
value: 'parent community'
|
||||||
|
}],
|
||||||
|
_links: {}
|
||||||
|
});
|
||||||
|
spyOn(rdbService, 'buildSingle').and.returnValue(createSuccessfulRemoteDataObject$(parentCommunity));
|
||||||
|
data = {
|
||||||
|
dso: Object.assign(new Community(), {
|
||||||
|
metadata: [{
|
||||||
|
key: 'dc.title',
|
||||||
|
value: 'child community'
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
_links: {
|
||||||
|
parentCommunity: {
|
||||||
|
href: 'child/parentCommunity'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
it('child level community cache refreshed', () => {
|
||||||
|
scheduler.schedule(() => (service as any).refreshCache(data));
|
||||||
|
scheduler.flush();
|
||||||
|
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith('a20da287-e174-466a-9926-f66as300d399');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -21,12 +21,14 @@ import {
|
|||||||
configureRequest,
|
configureRequest,
|
||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
getResponseFromEntry,
|
getResponseFromEntry,
|
||||||
|
getSucceededOrNoContentResponse,
|
||||||
getSucceededRemoteData
|
getSucceededRemoteData
|
||||||
} from '../shared/operators';
|
} from '../shared/operators';
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
import { RestResponse } from '../cache/response.models';
|
import { RestResponse } from '../cache/response.models';
|
||||||
import { Bitstream } from '../shared/bitstream.model';
|
import { Bitstream } from '../shared/bitstream.model';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
import {Collection} from '../shared/collection.model';
|
||||||
|
|
||||||
export abstract class ComColDataService<T extends CacheableObject> extends DataService<T> {
|
export abstract class ComColDataService<T extends CacheableObject> extends DataService<T> {
|
||||||
protected abstract cds: CommunityDataService;
|
protected abstract cds: CommunityDataService;
|
||||||
@@ -119,4 +121,23 @@ export abstract class ComColDataService<T extends CacheableObject> extends DataS
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public refreshCache(dso: T) {
|
||||||
|
const parentCommunityUrl = this.parentCommunityUrlLookup(dso as any);
|
||||||
|
if (!hasValue(parentCommunityUrl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.findByHref(parentCommunityUrl).pipe(
|
||||||
|
getSucceededOrNoContentResponse(),
|
||||||
|
take(1),
|
||||||
|
).subscribe((rd: RemoteData<any>) => {
|
||||||
|
const href = rd.hasSucceeded && !isEmpty(rd.payload.id) ? rd.payload.id : this.halService.getEndpoint('communities/search/top');
|
||||||
|
this.requestService.removeByHrefSubstring(href)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private parentCommunityUrlLookup(dso: Collection | Community) {
|
||||||
|
const parentCommunity = dso._links.parentCommunity;
|
||||||
|
return isNotEmpty(parentCommunity) ? parentCommunity.href : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { take, tap } from 'rxjs/operators';
|
import { filter, take, tap } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
@@ -11,7 +11,6 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
|
|||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { getFinishedRemoteData } from '../shared/operators';
|
|
||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
@@ -56,7 +55,7 @@ export class DsoRedirectDataService extends DataService<any> {
|
|||||||
findByIdAndIDType(id: string, identifierType = IdentifierType.UUID): Observable<RemoteData<FindByIDRequest>> {
|
findByIdAndIDType(id: string, identifierType = IdentifierType.UUID): Observable<RemoteData<FindByIDRequest>> {
|
||||||
this.setLinkPath(identifierType);
|
this.setLinkPath(identifierType);
|
||||||
return this.findById(id).pipe(
|
return this.findById(id).pipe(
|
||||||
getFinishedRemoteData(),
|
filter((response) => hasValue(response.error) || hasValue(response.payload)),
|
||||||
take(1),
|
take(1),
|
||||||
tap((response) => {
|
tap((response) => {
|
||||||
if (response.hasSucceeded) {
|
if (response.hasSucceeded) {
|
||||||
|
@@ -58,4 +58,8 @@ export class DSpaceObjectDataService {
|
|||||||
findById(uuid: string): Observable<RemoteData<DSpaceObject>> {
|
findById(uuid: string): Observable<RemoteData<DSpaceObject>> {
|
||||||
return this.dataService.findById(uuid);
|
return this.dataService.findById(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findByHref(href: string): Observable<RemoteData<DSpaceObject>> {
|
||||||
|
return this.dataService.findByHref(href);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,11 +12,12 @@ import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { GetRequest } from './request.models';
|
import { GetRequest } from './request.models';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { switchMap, take } from 'rxjs/operators';
|
import { switchMap, take, map } from 'rxjs/operators';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
||||||
import { PaginatedList } from './paginated-list';
|
import { PaginatedList } from './paginated-list';
|
||||||
import { ItemType } from '../shared/item-relationships/item-type.model';
|
import { ItemType } from '../shared/item-relationships/item-type.model';
|
||||||
|
import {getRemoteDataPayload, getSucceededRemoteData} from '../shared/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service handling all ItemType requests
|
* Service handling all ItemType requests
|
||||||
@@ -51,6 +52,20 @@ export class EntityTypeService extends DataService<ItemType> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a given entity type is the left type of a given relationship type, as an observable boolean
|
||||||
|
* @param relationshipType the relationship type for which to check whether the given entity type is the left type
|
||||||
|
* @param entityType the entity type for which to check whether it is the left type of the given relationship type
|
||||||
|
*/
|
||||||
|
isLeftType(relationshipType: RelationshipType, itemType: ItemType): Observable<boolean> {
|
||||||
|
|
||||||
|
return relationshipType.leftType.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((leftType) => leftType.uuid === itemType.uuid),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the allowed relationship types for an entity type
|
* Get the allowed relationship types for an entity type
|
||||||
* @param entityTypeId
|
* @param entityTypeId
|
||||||
|
@@ -19,7 +19,7 @@ import { FindListOptions, FindListRequest } from '../request.models';
|
|||||||
import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { RemoteData } from '../remote-data';
|
import { RemoteData } from '../remote-data';
|
||||||
import { PaginatedList } from '../paginated-list';
|
import { PaginatedList } from '../paginated-list';
|
||||||
import { find, map, switchMap, tap } from 'rxjs/operators';
|
import { catchError, find, map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { RequestParam } from '../../cache/models/request-param.model';
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
import { AuthorizationSearchParams } from './authorization-search-params';
|
import { AuthorizationSearchParams } from './authorization-search-params';
|
||||||
@@ -67,6 +67,7 @@ export class AuthorizationDataService extends DataService<Authorization> {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
catchError(() => observableOf(false)),
|
||||||
oneAuthorizationMatchesFeature(featureId)
|
oneAuthorizationMatchesFeature(featureId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,8 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-dat
|
|||||||
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||||
import { DsoPageFeatureGuard } from './dso-page-feature.guard';
|
import { DsoPageFeatureGuard } from './dso-page-feature.guard';
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { AuthService } from '../../../auth/auth.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test implementation of abstract class DsoPageAdministratorGuard
|
* Test implementation of abstract class DsoPageAdministratorGuard
|
||||||
@@ -14,8 +16,9 @@ class DsoPageFeatureGuardImpl extends DsoPageFeatureGuard<any> {
|
|||||||
constructor(protected resolver: Resolve<RemoteData<any>>,
|
constructor(protected resolver: Resolve<RemoteData<any>>,
|
||||||
protected authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
|
protected authService: AuthService,
|
||||||
protected featureID: FeatureID) {
|
protected featureID: FeatureID) {
|
||||||
super(resolver, authorizationService, router);
|
super(resolver, authorizationService, router, authService);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
@@ -27,6 +30,7 @@ describe('DsoPageAdministratorGuard', () => {
|
|||||||
let guard: DsoPageFeatureGuard<any>;
|
let guard: DsoPageFeatureGuard<any>;
|
||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let router: Router;
|
let router: Router;
|
||||||
|
let authService: AuthService;
|
||||||
let resolver: Resolve<RemoteData<any>>;
|
let resolver: Resolve<RemoteData<any>>;
|
||||||
let object: DSpaceObject;
|
let object: DSpaceObject;
|
||||||
|
|
||||||
@@ -44,7 +48,10 @@ describe('DsoPageAdministratorGuard', () => {
|
|||||||
resolver = jasmine.createSpyObj('resolver', {
|
resolver = jasmine.createSpyObj('resolver', {
|
||||||
resolve: createSuccessfulRemoteDataObject$(object)
|
resolve: createSuccessfulRemoteDataObject$(object)
|
||||||
});
|
});
|
||||||
guard = new DsoPageFeatureGuardImpl(resolver, authorizationService, router, undefined);
|
authService = jasmine.createSpyObj('authService', {
|
||||||
|
isAuthenticated: observableOf(true)
|
||||||
|
});
|
||||||
|
guard = new DsoPageFeatureGuardImpl(resolver, authorizationService, router, authService, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user