mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'CST-12791-merge-main-to-coar' into coar-CST-12791
This commit is contained in:
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@@ -43,11 +43,11 @@ jobs:
|
||||
steps:
|
||||
# https://github.com/actions/checkout
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# https://github.com/actions/setup-node
|
||||
- name: Install Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
# https://github.com/cypress-io/github-action
|
||||
# (NOTE: to run these e2e tests locally, just use 'ng e2e')
|
||||
- name: Run e2e tests (integration tests)
|
||||
uses: cypress-io/github-action@v5
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
# Run tests in Chrome, headless mode (default)
|
||||
browser: chrome
|
||||
@@ -191,7 +191,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Download artifacts from previous 'tests' job
|
||||
- name: Download coverage artifacts
|
||||
@@ -203,10 +203,14 @@ jobs:
|
||||
# Retry action: https://github.com/marketplace/actions/retry-action
|
||||
# Codecov action: https://github.com/codecov/codecov-action
|
||||
- name: Upload coverage to Codecov.io
|
||||
uses: Wandalen/wretry.action@v1.0.36
|
||||
uses: Wandalen/wretry.action@v1.3.0
|
||||
with:
|
||||
action: codecov/codecov-action@v3
|
||||
# Try upload 5 times max
|
||||
# Ensure codecov-action throws an error when it fails to upload
|
||||
# This allows us to auto-restart the action if an error is thrown
|
||||
with: |
|
||||
fail_ci_if_error: true
|
||||
# Try re-running action 5 times max
|
||||
attempt_limit: 5
|
||||
# Run again in 30 seconds
|
||||
attempt_delay: 30000
|
||||
|
2
.github/workflows/codescan.yml
vendored
2
.github/workflows/codescan.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
steps:
|
||||
# https://github.com/actions/checkout
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
# https://github.com/github/codeql-action
|
||||
|
151
.github/workflows/docker.yml
vendored
151
.github/workflows/docker.yml
vendored
@@ -3,6 +3,9 @@ name: Docker images
|
||||
|
||||
# Run this Build for all pushes to 'main' or maintenance branches, or tagged releases.
|
||||
# Also run for PRs to ensure PR doesn't break Docker build process
|
||||
# NOTE: uses "reusable-docker-build.yml" in DSpace/DSpace to actually build each of the Docker images
|
||||
# https://github.com/DSpace/DSpace/blob/main/.github/workflows/reusable-docker-build.yml
|
||||
#
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -15,82 +18,22 @@ on:
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
|
||||
env:
|
||||
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
|
||||
# For a new commit on default branch (main), use the literal tag 'latest' on Docker image.
|
||||
# For a new commit on other branches, use the branch name as the tag for Docker image.
|
||||
# For a new tag, copy that tag name as the tag for Docker image.
|
||||
IMAGE_TAGS: |
|
||||
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
|
||||
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
|
||||
type=ref,event=tag
|
||||
# Define default tag "flavor" for docker/metadata-action per
|
||||
# https://github.com/docker/metadata-action#flavor-input
|
||||
# We manage the 'latest' tag ourselves to the 'main' branch (see settings above)
|
||||
TAGS_FLAVOR: |
|
||||
latest=false
|
||||
# Architectures / Platforms for which we will build Docker images
|
||||
# If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work.
|
||||
# If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64.
|
||||
PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }}
|
||||
|
||||
|
||||
jobs:
|
||||
###############################################
|
||||
#############################################################
|
||||
# Build/Push the 'dspace/dspace-angular' image
|
||||
###############################################
|
||||
#############################################################
|
||||
dspace-angular:
|
||||
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
|
||||
if: github.repository == 'dspace/dspace-angular'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# https://github.com/actions/checkout
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# https://github.com/docker/setup-buildx-action
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
# https://github.com/docker/setup-qemu-action
|
||||
- name: Set up QEMU emulation to build for multiple architectures
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
# https://github.com/docker/login-action
|
||||
- name: Login to DockerHub
|
||||
# Only login if not a PR, as PRs only trigger a Docker build and not a push
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||
|
||||
# https://github.com/docker/metadata-action
|
||||
# Get Metadata for docker_build step below
|
||||
- name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular' image
|
||||
id: meta_build
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: dspace/dspace-angular
|
||||
tags: ${{ env.IMAGE_TAGS }}
|
||||
flavor: ${{ env.TAGS_FLAVOR }}
|
||||
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push 'dspace-angular' image
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
|
||||
# but we ONLY do an image push to DockerHub if it's NOT a PR
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
# Use tags / labels provided by 'docker/metadata-action' above
|
||||
tags: ${{ steps.meta_build.outputs.tags }}
|
||||
labels: ${{ steps.meta_build.outputs.labels }}
|
||||
# Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image
|
||||
uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@main
|
||||
with:
|
||||
build_id: dspace-angular
|
||||
image_name: dspace/dspace-angular
|
||||
dockerfile_path: ./Dockerfile
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||
|
||||
#############################################################
|
||||
# Build/Push the 'dspace/dspace-angular' image ('-dist' tag)
|
||||
@@ -98,53 +41,19 @@ jobs:
|
||||
dspace-angular-dist:
|
||||
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
|
||||
if: github.repository == 'dspace/dspace-angular'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
# https://github.com/actions/checkout
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# https://github.com/docker/setup-buildx-action
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
# https://github.com/docker/setup-qemu-action
|
||||
- name: Set up QEMU emulation to build for multiple architectures
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
# https://github.com/docker/login-action
|
||||
- name: Login to DockerHub
|
||||
# Only login if not a PR, as PRs only trigger a Docker build and not a push
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||
|
||||
# https://github.com/docker/metadata-action
|
||||
# Get Metadata for docker_build_dist step below
|
||||
- name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular-dist' image
|
||||
id: meta_build_dist
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: dspace/dspace-angular
|
||||
tags: ${{ env.IMAGE_TAGS }}
|
||||
# As this is a "dist" image, its tags are all suffixed with "-dist". Otherwise, it uses the same
|
||||
# tagging logic as the primary 'dspace/dspace-angular' image above.
|
||||
flavor: ${{ env.TAGS_FLAVOR }}
|
||||
suffix=-dist
|
||||
|
||||
- name: Build and push 'dspace-angular-dist' image
|
||||
id: docker_build_dist
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.dist
|
||||
platforms: ${{ env.PLATFORMS }}
|
||||
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
|
||||
# but we ONLY do an image push to DockerHub if it's NOT a PR
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
# Use tags / labels provided by 'docker/metadata-action' above
|
||||
tags: ${{ steps.meta_build_dist.outputs.tags }}
|
||||
labels: ${{ steps.meta_build_dist.outputs.labels }}
|
||||
# Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image
|
||||
uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@main
|
||||
with:
|
||||
build_id: dspace-angular-dist
|
||||
image_name: dspace/dspace-angular
|
||||
dockerfile_path: ./Dockerfile.dist
|
||||
# As this is a "dist" image, its tags are all suffixed with "-dist". Otherwise, it uses the same
|
||||
# tagging logic as the primary 'dspace/dspace-angular' image above.
|
||||
tags_flavor: suffix=-dist
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||
# Enable redeploy of sandbox & demo if the branch for this image matches the deployment branch of
|
||||
# these sites as specified in reusable-docker-build.xml
|
||||
REDEPLOY_SANDBOX_URL: ${{ secrets.REDEPLOY_SANDBOX_URL }}
|
||||
REDEPLOY_DEMO_URL: ${{ secrets.REDEPLOY_DEMO_URL }}
|
@@ -23,11 +23,11 @@ jobs:
|
||||
if: github.event.pull_request.merged
|
||||
steps:
|
||||
# Checkout code
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
# Port PR to other branch (ONLY if labeled with "port to")
|
||||
# See https://github.com/korthout/backport-action
|
||||
- name: Create backport pull requests
|
||||
uses: korthout/backport-action@v1
|
||||
uses: korthout/backport-action@v2
|
||||
with:
|
||||
# Trigger based on a "port to [branch]" label on PR
|
||||
# (This label must specify the branch name to port to)
|
||||
@@ -39,6 +39,8 @@ jobs:
|
||||
# Copy all labels from original PR to (newly created) port PR
|
||||
# NOTE: The labels matching 'label_pattern' are automatically excluded
|
||||
copy_labels_pattern: '.*'
|
||||
# Skip any merge commits in the ported PR. This means only non-merge commits are cherry-picked to the new PR
|
||||
merge_commits: 'skip'
|
||||
# Use a personal access token (PAT) to create PR as 'dspace-bot' user.
|
||||
# A PAT is required in order for the new PR to trigger its own actions (for CI checks)
|
||||
github_token: ${{ secrets.PR_PORT_TOKEN }}
|
2
.github/workflows/pull_request_opened.yml
vendored
2
.github/workflows/pull_request_opened.yml
vendored
@@ -21,4 +21,4 @@ jobs:
|
||||
# Assign the PR to whomever created it. This is useful for visualizing assignments on project boards
|
||||
# See https://github.com/toshimaru/auto-author-assign
|
||||
- name: Assign PR to creator
|
||||
uses: toshimaru/auto-author-assign@v1.6.2
|
||||
uses: toshimaru/auto-author-assign@v2.0.1
|
||||
|
@@ -157,8 +157,8 @@ DSPACE_UI_SSL => DSPACE_SSL
|
||||
|
||||
The same settings can also be overwritten by setting system environment variables instead, E.g.:
|
||||
```bash
|
||||
export DSPACE_HOST=api7.dspace.org
|
||||
export DSPACE_UI_PORT=4200
|
||||
export DSPACE_HOST=demo.dspace.org
|
||||
export DSPACE_UI_PORT=4000
|
||||
```
|
||||
|
||||
The priority works as follows: **environment variable** overrides **variable in `.env` file** overrides external config set by `DSPACE_APP_CONFIG_PATH` overrides **`config.(prod or dev).yml`**
|
||||
@@ -288,7 +288,7 @@ E2E tests (aka integration tests) use [Cypress.io](https://www.cypress.io/). Con
|
||||
The test files can be found in the `./cypress/integration/` folder.
|
||||
|
||||
Before you can run e2e tests, two things are REQUIRED:
|
||||
1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo REST API (https://api7.dspace.org/server/), as that server is uncontrolled and may have content added/removed at any time.
|
||||
1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo/sandbox REST API (https://demo.dspace.org/server/ or https://sandbox.dspace.org/server/), as those sites may have content added/removed at any time.
|
||||
* After starting up your backend on localhost, make sure either your `config.prod.yml` or `config.dev.yml` has its `rest` settings defined to use that localhost backend.
|
||||
* If you'd prefer, you may instead use environment variables as described at [Configuring](#configuring). For example:
|
||||
```
|
||||
|
@@ -22,7 +22,7 @@ ui:
|
||||
# 'synced' with the 'dspace.server.url' setting in your backend's local.cfg.
|
||||
rest:
|
||||
ssl: true
|
||||
host: api7.dspace.org
|
||||
host: sandbox.dspace.org
|
||||
port: 443
|
||||
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||
nameSpace: /server
|
||||
@@ -208,6 +208,9 @@ languages:
|
||||
- code: pt-BR
|
||||
label: Português do Brasil
|
||||
active: true
|
||||
- code: sr-lat
|
||||
label: Srpski (lat)
|
||||
active: true
|
||||
- code: fi
|
||||
label: Suomi
|
||||
active: true
|
||||
@@ -232,6 +235,9 @@ languages:
|
||||
- code: el
|
||||
label: Ελληνικά
|
||||
active: true
|
||||
- code: sr-cyr
|
||||
label: Српски
|
||||
active: true
|
||||
- code: uk
|
||||
label: Yкраї́нська
|
||||
active: true
|
||||
@@ -292,33 +298,33 @@ themes:
|
||||
#
|
||||
# # A theme with a handle property will match the community, collection or item with the given
|
||||
# # handle, and all collections and/or items within it
|
||||
# - name: 'custom',
|
||||
# handle: '10673/1233'
|
||||
# - name: custom
|
||||
# handle: 10673/1233
|
||||
#
|
||||
# # A theme with a regex property will match the route using a regular expression. If it
|
||||
# # matches the route for a community or collection it will also apply to all collections
|
||||
# # and/or items within it
|
||||
# - name: 'custom',
|
||||
# regex: 'collections\/e8043bc2.*'
|
||||
# - name: custom
|
||||
# regex: collections\/e8043bc2.*
|
||||
#
|
||||
# # A theme with a uuid property will match the community, collection or item with the given
|
||||
# # ID, and all collections and/or items within it
|
||||
# - name: 'custom',
|
||||
# uuid: '0958c910-2037-42a9-81c7-dca80e3892b4'
|
||||
# - name: custom
|
||||
# uuid: 0958c910-2037-42a9-81c7-dca80e3892b4
|
||||
#
|
||||
# # The extends property specifies an ancestor theme (by name). Whenever a themed component is not found
|
||||
# # in the current theme, its ancestor theme(s) will be checked recursively before falling back to default.
|
||||
# - name: 'custom-A',
|
||||
# extends: 'custom-B',
|
||||
# - name: custom-A
|
||||
# extends: custom-B
|
||||
# # Any of the matching properties above can be used
|
||||
# handle: '10673/34'
|
||||
# handle: 10673/34
|
||||
#
|
||||
# - name: 'custom-B',
|
||||
# extends: 'custom',
|
||||
# handle: '10673/12'
|
||||
# - name: custom-B
|
||||
# extends: custom
|
||||
# handle: 10673/12
|
||||
#
|
||||
# # A theme with only a name will match every route
|
||||
# name: 'custom'
|
||||
# name: custom
|
||||
#
|
||||
# # This theme will use the default bootstrap styling for DSpace components
|
||||
# - name: BASE_THEME_NAME
|
||||
@@ -379,4 +385,4 @@ vocabularies:
|
||||
# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query.
|
||||
comcolSelectionSort:
|
||||
sortField: 'dc.title'
|
||||
sortDirection: 'ASC'
|
||||
sortDirection: 'ASC'
|
||||
|
@@ -1,5 +1,5 @@
|
||||
rest:
|
||||
ssl: true
|
||||
host: api7.dspace.org
|
||||
host: sandbox.dspace.org
|
||||
port: 443
|
||||
nameSpace: /server
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { Options } from 'cypress-axe';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Community List Page', () => {
|
||||
@@ -13,13 +12,6 @@ describe('Community List Page', () => {
|
||||
cy.get('[data-test="expand-button"]').click({ multiple: true });
|
||||
|
||||
// Analyze <ds-community-list-page> for accessibility issues
|
||||
// Disable heading-order checks until it is fixed
|
||||
testA11y('ds-community-list-page',
|
||||
{
|
||||
rules: {
|
||||
'heading-order': { enabled: false }
|
||||
}
|
||||
} as Options
|
||||
);
|
||||
testA11y('ds-community-list-page');
|
||||
});
|
||||
});
|
||||
|
@@ -8,12 +8,6 @@ describe('Header', () => {
|
||||
cy.get('ds-header').should('be.visible');
|
||||
|
||||
// Analyze <ds-header> for accessibility
|
||||
testA11y({
|
||||
include: ['ds-header'],
|
||||
exclude: [
|
||||
['#search-navbar-container'], // search in navbar has duplicative ID. Will be fixed in #1174
|
||||
['.dropdownLogin'] // "Log in" link has color contrast issues. Will be fixed in #1149
|
||||
],
|
||||
});
|
||||
testA11y('ds-header');
|
||||
});
|
||||
});
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { Options } from 'cypress-axe';
|
||||
import { TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
@@ -19,13 +18,16 @@ describe('Item Page', () => {
|
||||
cy.get('ds-item-page').should('be.visible');
|
||||
|
||||
// Analyze <ds-item-page> for accessibility issues
|
||||
// Disable heading-order checks until it is fixed
|
||||
testA11y('ds-item-page',
|
||||
{
|
||||
rules: {
|
||||
'heading-order': { enabled: false }
|
||||
}
|
||||
} as Options
|
||||
);
|
||||
testA11y('ds-item-page');
|
||||
});
|
||||
|
||||
it('should pass accessibility tests on full item page', () => {
|
||||
cy.visit(ENTITYPAGE + '/full');
|
||||
|
||||
// <ds-full-item-page> tag must be loaded
|
||||
cy.get('ds-full-item-page').should('be.visible');
|
||||
|
||||
// Analyze <ds-full-item-page> for accessibility issues
|
||||
testA11y('ds-full-item-page');
|
||||
});
|
||||
});
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
const page = {
|
||||
openLoginMenu() {
|
||||
@@ -123,4 +124,15 @@ describe('Login Modal', () => {
|
||||
cy.location('pathname').should('eq', '/forgot');
|
||||
cy.get('ds-forgot-email').should('exist');
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/');
|
||||
|
||||
page.openLoginMenu();
|
||||
|
||||
cy.get('ds-log-in').should('exist');
|
||||
|
||||
// Analyze <ds-log-in> for accessibility issues
|
||||
testA11y('ds-log-in');
|
||||
});
|
||||
});
|
||||
|
@@ -19,21 +19,7 @@ describe('My DSpace page', () => {
|
||||
cy.get('.filter-toggle').click({ multiple: true });
|
||||
|
||||
// Analyze <ds-my-dspace-page> for accessibility issues
|
||||
testA11y(
|
||||
{
|
||||
include: ['ds-my-dspace-page'],
|
||||
exclude: [
|
||||
['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175
|
||||
],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
// Search filters fail these two "moderate" impact rules
|
||||
'heading-order': { enabled: false },
|
||||
'landmark-unique': { enabled: false }
|
||||
}
|
||||
} as Options
|
||||
);
|
||||
testA11y('ds-my-dspace-page');
|
||||
});
|
||||
|
||||
it('should have a working detailed view that passes accessibility tests', () => {
|
||||
|
@@ -1,8 +1,13 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('PageNotFound', () => {
|
||||
it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => {
|
||||
// request an invalid page (UUIDs at root path aren't valid)
|
||||
cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false });
|
||||
cy.get('ds-pagenotfound').should('be.visible');
|
||||
|
||||
// Analyze <ds-pagenotfound> for accessibility issues
|
||||
testA11y('ds-pagenotfound');
|
||||
});
|
||||
|
||||
it('should not contain element ds-pagenotfound when navigating to existing page', () => {
|
||||
|
@@ -27,21 +27,7 @@ describe('Search Page', () => {
|
||||
cy.get('[data-test="filter-toggle"]').click({ multiple: true });
|
||||
|
||||
// Analyze <ds-search-page> for accessibility issues
|
||||
testA11y(
|
||||
{
|
||||
include: ['ds-search-page'],
|
||||
exclude: [
|
||||
['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175
|
||||
],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
// Search filters fail these two "moderate" impact rules
|
||||
'heading-order': { enabled: false },
|
||||
'landmark-unique': { enabled: false }
|
||||
}
|
||||
} as Options
|
||||
);
|
||||
testA11y('ds-search-page');
|
||||
});
|
||||
|
||||
it('should have a working grid view that passes accessibility tests', () => {
|
||||
|
@@ -177,6 +177,8 @@ function generateViewEvent(uuid: string, dsoType: string): void {
|
||||
[XSRF_REQUEST_HEADER] : csrfToken,
|
||||
// use a known public IP address to avoid being seen as a "bot"
|
||||
'X-Forwarded-For': '1.1.1.1',
|
||||
// Use a user-agent of a Firefox browser on Windows. This again avoids being seen as a "bot"
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0',
|
||||
},
|
||||
//form: true, // indicates the body should be form urlencoded
|
||||
body: { targetId: uuid, targetType: dsoType },
|
||||
|
@@ -101,8 +101,8 @@ and the backend at http://localhost:8080/server/
|
||||
|
||||
## Run DSpace Angular dist build with DSpace Demo site backend
|
||||
|
||||
This allows you to run the Angular UI in *production* mode, pointing it at the demo backend
|
||||
(https://api7.dspace.org/server/).
|
||||
This allows you to run the Angular UI in *production* mode, pointing it at the demo or sandbox backend
|
||||
(https://demo.dspace.org/server/ or https://sandbox.dspace.org/server/).
|
||||
|
||||
```
|
||||
docker-compose -f docker/docker-compose-dist.yml pull
|
||||
|
@@ -24,7 +24,7 @@ services:
|
||||
# This is because Server Side Rendering (SSR) currently requires a public URL,
|
||||
# see this bug: https://github.com/DSpace/dspace-angular/issues/1485
|
||||
DSPACE_REST_SSL: 'true'
|
||||
DSPACE_REST_HOST: api7.dspace.org
|
||||
DSPACE_REST_HOST: sandbox.dspace.org
|
||||
DSPACE_REST_PORT: 443
|
||||
DSPACE_REST_NAMESPACE: /server
|
||||
image: dspace/dspace-angular:${DSPACE_VER:-latest}-dist
|
||||
|
@@ -48,7 +48,7 @@ dspace-angular connects to your DSpace installation by using its REST endpoint.
|
||||
```yaml
|
||||
rest:
|
||||
ssl: true
|
||||
host: api7.dspace.org
|
||||
host: demo.dspace.org
|
||||
port: 443
|
||||
nameSpace: /server
|
||||
}
|
||||
@@ -57,7 +57,7 @@ rest:
|
||||
Alternately you can set the following environment variables. If any of these are set, it will override all configuration files:
|
||||
```
|
||||
DSPACE_REST_SSL=true
|
||||
DSPACE_REST_HOST=api7.dspace.org
|
||||
DSPACE_REST_HOST=demo.dspace.org
|
||||
DSPACE_REST_PORT=443
|
||||
DSPACE_REST_NAMESPACE=/server
|
||||
```
|
||||
|
17
package.json
17
package.json
@@ -15,14 +15,14 @@
|
||||
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
|
||||
"build": "ng build --configuration development",
|
||||
"build:stats": "ng build --stats-json",
|
||||
"build:prod": "yarn run build:ssr",
|
||||
"build:prod": "cross-env NODE_ENV=production yarn run build:ssr",
|
||||
"build:ssr": "ng build --configuration production && ng run dspace-angular:server:production",
|
||||
"test": "ng test --source-map=true --watch=false --configuration test",
|
||||
"test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"",
|
||||
"test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage",
|
||||
"lint": "ng lint",
|
||||
"lint-fix": "ng lint --fix=true",
|
||||
"e2e": "ng e2e",
|
||||
"e2e": "cross-env NODE_ENV=production ng e2e",
|
||||
"clean:dev:config": "rimraf src/assets/config.json",
|
||||
"clean:coverage": "rimraf coverage",
|
||||
"clean:dist": "rimraf dist",
|
||||
@@ -82,7 +82,7 @@
|
||||
"@types/grecaptcha": "^3.0.4",
|
||||
"angular-idle-preload": "3.0.0",
|
||||
"angulartics2": "^12.2.0",
|
||||
"axios": "^0.27.2",
|
||||
"axios": "^1.6.0",
|
||||
"bootstrap": "^4.6.1",
|
||||
"cerialize": "0.1.18",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -99,6 +99,7 @@
|
||||
"fast-json-patch": "^3.1.1",
|
||||
"filesize": "^6.1.0",
|
||||
"http-proxy-middleware": "^1.0.5",
|
||||
"http-terminator": "^3.2.0",
|
||||
"isbot": "^3.6.10",
|
||||
"js-cookie": "2.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
@@ -116,12 +117,12 @@
|
||||
"morgan": "^1.10.0",
|
||||
"ng-mocks": "^14.10.0",
|
||||
"ng2-file-upload": "1.4.0",
|
||||
"ng2-nouislider": "^1.8.3",
|
||||
"ng2-nouislider": "^2.0.0",
|
||||
"ngx-infinite-scroll": "^15.0.0",
|
||||
"ngx-pagination": "6.0.3",
|
||||
"ngx-sortablejs": "^11.1.0",
|
||||
"ngx-ui-switch": "^14.0.3",
|
||||
"nouislider": "^14.6.3",
|
||||
"ngx-ui-switch": "^14.1.0",
|
||||
"nouislider": "^15.7.1",
|
||||
"pem": "1.14.7",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
@@ -159,11 +160,11 @@
|
||||
"@types/sanitize-html": "^2.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
||||
"@typescript-eslint/parser": "^5.59.1",
|
||||
"axe-core": "^4.7.0",
|
||||
"axe-core": "^4.7.2",
|
||||
"compression-webpack-plugin": "^9.2.0",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "12.10.0",
|
||||
"cypress": "12.17.4",
|
||||
"cypress-axe": "^1.4.0",
|
||||
"deep-freeze": "0.0.1",
|
||||
"eslint": "^8.39.0",
|
||||
|
43
server.ts
43
server.ts
@@ -32,6 +32,7 @@ import isbot from 'isbot';
|
||||
import { createCertificate } from 'pem';
|
||||
import { createServer } from 'https';
|
||||
import { json } from 'body-parser';
|
||||
import { createHttpTerminator } from 'http-terminator';
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
@@ -320,22 +321,23 @@ function initCache() {
|
||||
if (botCacheEnabled()) {
|
||||
// Initialize a new "least-recently-used" item cache (where least recently used pages are removed first)
|
||||
// See https://www.npmjs.com/package/lru-cache
|
||||
// When enabled, each page defaults to expiring after 1 day
|
||||
// When enabled, each page defaults to expiring after 1 day (defined in default-app-config.ts)
|
||||
botCache = new LRU( {
|
||||
max: environment.cache.serverSide.botCache.max,
|
||||
ttl: environment.cache.serverSide.botCache.timeToLive || 24 * 60 * 60 * 1000, // 1 day
|
||||
allowStale: environment.cache.serverSide.botCache.allowStale ?? true // if object is stale, return stale value before deleting
|
||||
ttl: environment.cache.serverSide.botCache.timeToLive,
|
||||
allowStale: environment.cache.serverSide.botCache.allowStale
|
||||
});
|
||||
}
|
||||
|
||||
if (anonymousCacheEnabled()) {
|
||||
// NOTE: While caches may share SSR pages, this cache must be kept separately because the timeToLive
|
||||
// may expire pages more frequently.
|
||||
// When enabled, each page defaults to expiring after 10 seconds (to minimize anonymous users seeing out-of-date content)
|
||||
// When enabled, each page defaults to expiring after 10 seconds (defined in default-app-config.ts)
|
||||
// to minimize anonymous users seeing out-of-date content
|
||||
anonymousCache = new LRU( {
|
||||
max: environment.cache.serverSide.anonymousCache.max,
|
||||
ttl: environment.cache.serverSide.anonymousCache.timeToLive || 10 * 1000, // 10 seconds
|
||||
allowStale: environment.cache.serverSide.anonymousCache.allowStale ?? true // if object is stale, return stale value before deleting
|
||||
ttl: environment.cache.serverSide.anonymousCache.timeToLive,
|
||||
allowStale: environment.cache.serverSide.anonymousCache.allowStale
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -487,7 +489,7 @@ function saveToCache(req, page: any) {
|
||||
*/
|
||||
function hasNotSucceeded(statusCode) {
|
||||
const rgx = new RegExp(/^20+/);
|
||||
return !rgx.test(statusCode)
|
||||
return !rgx.test(statusCode);
|
||||
}
|
||||
|
||||
function retrieveHeaders(response) {
|
||||
@@ -525,23 +527,46 @@ function serverStarted() {
|
||||
* @param keys SSL credentials
|
||||
*/
|
||||
function createHttpsServer(keys) {
|
||||
createServer({
|
||||
const listener = createServer({
|
||||
key: keys.serviceKey,
|
||||
cert: keys.certificate
|
||||
}, app).listen(environment.ui.port, environment.ui.host, () => {
|
||||
serverStarted();
|
||||
});
|
||||
|
||||
// Graceful shutdown when signalled
|
||||
const terminator = createHttpTerminator({server: listener});
|
||||
process.on('SIGINT', () => {
|
||||
void (async ()=> {
|
||||
console.debug('Closing HTTPS server on signal');
|
||||
await terminator.terminate().catch(e => { console.error(e); });
|
||||
console.debug('HTTPS server closed');
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTTP server with the configured port and host.
|
||||
*/
|
||||
function run() {
|
||||
const port = environment.ui.port || 4000;
|
||||
const host = environment.ui.host || '/';
|
||||
|
||||
// Start up the Node server
|
||||
const server = app();
|
||||
server.listen(port, host, () => {
|
||||
const listener = server.listen(port, host, () => {
|
||||
serverStarted();
|
||||
});
|
||||
|
||||
// Graceful shutdown when signalled
|
||||
const terminator = createHttpTerminator({server: listener});
|
||||
process.on('SIGINT', () => {
|
||||
void (async () => {
|
||||
console.debug('Closing HTTP server on signal');
|
||||
await terminator.terminate().catch(e => { console.error(e); });
|
||||
console.debug('HTTP server closed.');return undefined;
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
function start() {
|
||||
|
@@ -1,12 +1,22 @@
|
||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
import { getAccessControlModuleRoute } from '../app-routing-paths';
|
||||
|
||||
export const GROUP_EDIT_PATH = 'groups';
|
||||
export const EPERSON_PATH = 'epeople';
|
||||
|
||||
export function getEPersonsRoute(): string {
|
||||
return new URLCombiner(getAccessControlModuleRoute(), EPERSON_PATH).toString();
|
||||
}
|
||||
|
||||
export function getEPersonEditRoute(id: string): string {
|
||||
return new URLCombiner(getEPersonsRoute(), id, 'edit').toString();
|
||||
}
|
||||
|
||||
export const GROUP_PATH = 'groups';
|
||||
|
||||
export function getGroupsRoute() {
|
||||
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH).toString();
|
||||
return new URLCombiner(getAccessControlModuleRoute(), GROUP_PATH).toString();
|
||||
}
|
||||
|
||||
export function getGroupEditRoute(id: string) {
|
||||
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH, id).toString();
|
||||
return new URLCombiner(getGroupsRoute(), id, 'edit').toString();
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ import { RouterModule } from '@angular/router';
|
||||
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
|
||||
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
|
||||
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
||||
import { GROUP_EDIT_PATH } from './access-control-routing-paths';
|
||||
import { EPERSON_PATH, GROUP_PATH } from './access-control-routing-paths';
|
||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { GroupPageGuard } from './group-registry/group-page.guard';
|
||||
import {
|
||||
@@ -13,12 +13,14 @@ import {
|
||||
SiteAdministratorGuard
|
||||
} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
||||
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
|
||||
import { EPersonResolver } from './epeople-registry/eperson-resolver.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: 'epeople',
|
||||
path: EPERSON_PATH,
|
||||
component: EPeopleRegistryComponent,
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver
|
||||
@@ -27,7 +29,26 @@ import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
||||
canActivate: [SiteAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: GROUP_EDIT_PATH,
|
||||
path: `${EPERSON_PATH}/create`,
|
||||
component: EPersonFormComponent,
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver,
|
||||
},
|
||||
data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' },
|
||||
canActivate: [SiteAdministratorGuard],
|
||||
},
|
||||
{
|
||||
path: `${EPERSON_PATH}/:id/edit`,
|
||||
component: EPersonFormComponent,
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver,
|
||||
ePerson: EPersonResolver,
|
||||
},
|
||||
data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' },
|
||||
canActivate: [SiteAdministratorGuard],
|
||||
},
|
||||
{
|
||||
path: GROUP_PATH,
|
||||
component: GroupsRegistryComponent,
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver
|
||||
@@ -36,7 +57,7 @@ import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
||||
canActivate: [GroupAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: `${GROUP_EDIT_PATH}/newGroup`,
|
||||
path: `${GROUP_PATH}/create`,
|
||||
component: GroupFormComponent,
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver
|
||||
@@ -45,7 +66,7 @@ import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
||||
canActivate: [GroupAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: `${GROUP_EDIT_PATH}/:groupId`,
|
||||
path: `${GROUP_PATH}/:groupId/edit`,
|
||||
component: GroupFormComponent,
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver
|
||||
|
@@ -1,15 +1,15 @@
|
||||
<ngb-accordion #acc="ngbAccordion" [activeIds]="'browse'">
|
||||
<ngb-panel [id]="'browse'">
|
||||
<ng-template ngbPanelHeader>
|
||||
<div class="w-100 d-flex justify-content-between collapse-toggle" ngbPanelToggle (click)="acc.toggle('browse')"
|
||||
<div class="w-100 d-flex gap-3 justify-content-between collapse-toggle" ngbPanelToggle (click)="acc.toggle('browse')"
|
||||
data-test="browse">
|
||||
<button type="button" class="btn btn-link p-0" (click)="$event.preventDefault()"
|
||||
[attr.aria-expanded]="!acc.isExpanded('browse')"
|
||||
aria-controls="collapsePanels">
|
||||
[attr.aria-expanded]="acc.isExpanded('browse')"
|
||||
aria-controls="bulk-access-browse-panel-content">
|
||||
{{ 'admin.access-control.bulk-access-browse.header' | translate }}
|
||||
</button>
|
||||
<div class="text-right d-flex">
|
||||
<div class="ml-3 d-inline-block">
|
||||
<div class="text-right d-flex gap-2">
|
||||
<div class="d-flex my-auto">
|
||||
<span *ngIf="acc.isExpanded('browse')" class="fas fa-chevron-up fa-fw"></span>
|
||||
<span *ngIf="!acc.isExpanded('browse')" class="fas fa-chevron-down fa-fw"></span>
|
||||
</div>
|
||||
@@ -17,51 +17,53 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activateId" class="nav-pills">
|
||||
<li [ngbNavItem]="'search'">
|
||||
<a ngbNavLink>{{'admin.access-control.bulk-access-browse.search.header' | translate}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="mx-n3">
|
||||
<ds-themed-search [configuration]="'administrativeBulkAccess'"
|
||||
[selectable]="true"
|
||||
[selectionConfig]="{ repeatable: true, listId: listId }"
|
||||
[showThumbnails]="false"></ds-themed-search>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="'selected'">
|
||||
<a ngbNavLink>
|
||||
{{'admin.access-control.bulk-access-browse.selected.header' | translate: {number: ((objectsSelected$ | async)?.payload?.totalElements) ? (objectsSelected$ | async)?.payload?.totalElements : '0'} }}
|
||||
</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ds-pagination
|
||||
[paginationOptions]="(paginationOptions$ | async)"
|
||||
[pageInfoState]="(objectsSelected$|async)?.payload.pageInfo"
|
||||
[collectionSize]="(objectsSelected$|async)?.payload?.totalElements"
|
||||
[objects]="(objectsSelected$|async)"
|
||||
[showPaginator]="false"
|
||||
(prev)="pagePrev()"
|
||||
(next)="pageNext()">
|
||||
<ul *ngIf="(objectsSelected$|async)?.hasSucceeded" class="list-unstyled ml-4">
|
||||
<li *ngFor='let object of (objectsSelected$|async)?.payload?.page | paginate: { itemsPerPage: (paginationOptions$ | async).pageSize,
|
||||
currentPage: (paginationOptions$ | async).currentPage, totalItems: (objectsSelected$|async)?.payload?.page.length }; let i = index; let last = last '
|
||||
class="mt-4 mb-4 d-flex"
|
||||
[attr.data-test]="'list-object' | dsBrowserOnly">
|
||||
<ds-selectable-list-item-control [index]="i"
|
||||
[object]="object"
|
||||
[selectionConfig]="{ repeatable: true, listId: listId }"></ds-selectable-list-item-control>
|
||||
<ds-listable-object-component-loader [listID]="listId"
|
||||
[index]="i"
|
||||
[object]="object"
|
||||
[showThumbnails]="false"
|
||||
[viewMode]="'list'"></ds-listable-object-component-loader>
|
||||
</li>
|
||||
</ul>
|
||||
</ds-pagination>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="mt-5"></div>
|
||||
<div id="bulk-access-browse-panel-content">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activateId" class="nav-pills">
|
||||
<li [ngbNavItem]="'search'" role="presentation">
|
||||
<a ngbNavLink>{{'admin.access-control.bulk-access-browse.search.header' | translate}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="mx-n3">
|
||||
<ds-themed-search [configuration]="'administrativeBulkAccess'"
|
||||
[selectable]="true"
|
||||
[selectionConfig]="{ repeatable: true, listId: listId }"
|
||||
[showThumbnails]="false"></ds-themed-search>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="'selected'" role="presentation">
|
||||
<a ngbNavLink>
|
||||
{{'admin.access-control.bulk-access-browse.selected.header' | translate: {number: ((objectsSelected$ | async)?.payload?.totalElements) ? (objectsSelected$ | async)?.payload?.totalElements : '0'} }}
|
||||
</a>
|
||||
<ng-template ngbNavContent>
|
||||
<ds-pagination
|
||||
[paginationOptions]="(paginationOptions$ | async)"
|
||||
[pageInfoState]="(objectsSelected$|async)?.payload.pageInfo"
|
||||
[collectionSize]="(objectsSelected$|async)?.payload?.totalElements"
|
||||
[objects]="(objectsSelected$|async)"
|
||||
[showPaginator]="false"
|
||||
(prev)="pagePrev()"
|
||||
(next)="pageNext()">
|
||||
<ul *ngIf="(objectsSelected$|async)?.hasSucceeded" class="list-unstyled ml-4">
|
||||
<li *ngFor='let object of (objectsSelected$|async)?.payload?.page | paginate: { itemsPerPage: (paginationOptions$ | async).pageSize,
|
||||
currentPage: (paginationOptions$ | async).currentPage, totalItems: (objectsSelected$|async)?.payload?.page.length }; let i = index; let last = last '
|
||||
class="mt-4 mb-4 d-flex"
|
||||
[attr.data-test]="'list-object' | dsBrowserOnly">
|
||||
<ds-selectable-list-item-control [index]="i"
|
||||
[object]="object"
|
||||
[selectionConfig]="{ repeatable: true, listId: listId }"></ds-selectable-list-item-control>
|
||||
<ds-listable-object-component-loader [listID]="listId"
|
||||
[index]="i"
|
||||
[object]="object"
|
||||
[showThumbnails]="false"
|
||||
[viewMode]="'list'"></ds-listable-object-component-loader>
|
||||
</li>
|
||||
</ul>
|
||||
</ds-pagination>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="mt-5"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
</ngb-accordion>
|
||||
|
@@ -1,13 +1,13 @@
|
||||
<ngb-accordion #acc="ngbAccordion" [activeIds]="'settings'">
|
||||
<ngb-panel [id]="'settings'">
|
||||
<ng-template ngbPanelHeader>
|
||||
<div class="w-100 d-flex justify-content-between collapse-toggle" ngbPanelToggle (click)="acc.toggle('settings')" data-test="settings">
|
||||
<button type="button" class="btn btn-link p-0" (click)="$event.preventDefault()" [attr.aria-expanded]="!acc.isExpanded('browse')"
|
||||
aria-controls="collapsePanels">
|
||||
<div class="w-100 d-flex gap-3 justify-content-between collapse-toggle" ngbPanelToggle (click)="acc.toggle('settings')" data-test="settings">
|
||||
<button type="button" class="btn btn-link p-0" (click)="$event.preventDefault()" [attr.aria-expanded]="acc.isExpanded('settings')"
|
||||
aria-controls="bulk-access-settings-panel-content">
|
||||
{{ 'admin.access-control.bulk-access-settings.header' | translate }}
|
||||
</button>
|
||||
<div class="text-right d-flex">
|
||||
<div class="ml-3 d-inline-block">
|
||||
<div class="text-right d-flex gap-2">
|
||||
<div class="d-flex my-auto">
|
||||
<span *ngIf="acc.isExpanded('settings')" class="fas fa-chevron-up fa-fw"></span>
|
||||
<span *ngIf="!acc.isExpanded('settings')" class="fas fa-chevron-down fa-fw"></span>
|
||||
</div>
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<ds-access-control-form-container #dsAccessControlForm [showSubmit]="false"></ds-access-control-form-container>
|
||||
<ds-access-control-form-container id="bulk-access-settings-panel-content" #dsAccessControlForm [showSubmit]="false"></ds-access-control-form-container>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
</ngb-accordion>
|
||||
|
@@ -2,98 +2,93 @@
|
||||
<div class="epeople-registry row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between border-bottom mb-3">
|
||||
<h2 id="header" class="pb-2">{{labelPrefix + 'head' | translate}}</h2>
|
||||
<h1 id="header" class="pb-2">{{labelPrefix + 'head' | translate}}</h1>
|
||||
|
||||
<div *ngIf="!isEPersonFormShown">
|
||||
<div>
|
||||
<button class="mr-auto btn btn-success addEPerson-button"
|
||||
(click)="isEPersonFormShown = true">
|
||||
[routerLink]="'create'">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span class="d-none d-sm-inline ml-1">{{labelPrefix + 'button.add' | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ds-eperson-form *ngIf="isEPersonFormShown" (submitForm)="reset()"
|
||||
(cancelForm)="isEPersonFormShown = false"></ds-eperson-form>
|
||||
|
||||
<div *ngIf="!isEPersonFormShown">
|
||||
<h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | translate}}
|
||||
|
||||
</h3>
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||
<div>
|
||||
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
||||
<option value="metadata">{{labelPrefix + 'search.scope.metadata' | translate}}</option>
|
||||
<option value="email">{{labelPrefix + 'search.scope.email' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-grow-1 mr-3 ml-3">
|
||||
<div class="form-group input-group">
|
||||
<input type="text" name="query" id="query" formControlName="query"
|
||||
class="form-control" [attr.aria-label]="labelPrefix + 'search.placeholder' | translate"
|
||||
[placeholder]="(labelPrefix + 'search.placeholder' | translate)">
|
||||
<span class="input-group-append">
|
||||
<h2 id="search" class="border-bottom pb-2">
|
||||
{{labelPrefix + 'search.head' | translate}}
|
||||
</h2>
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||
<div>
|
||||
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
||||
<option value="metadata">{{labelPrefix + 'search.scope.metadata' | translate}}</option>
|
||||
<option value="email">{{labelPrefix + 'search.scope.email' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-grow-1 mr-3 ml-3">
|
||||
<div class="form-group input-group">
|
||||
<input type="text" name="query" id="query" formControlName="query"
|
||||
class="form-control" [attr.aria-label]="labelPrefix + 'search.placeholder' | translate"
|
||||
[placeholder]="(labelPrefix + 'search.placeholder' | translate)">
|
||||
<span class="input-group-append">
|
||||
<button type="submit" class="search-button btn btn-primary">
|
||||
<i class="fas fa-search"></i> {{ labelPrefix + 'search.button' | translate }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button (click)="clearFormAndResetResult();"
|
||||
class="search-button btn btn-secondary">{{labelPrefix + 'button.see-all' | translate}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-themed-loading *ngIf="searching$ | async"></ds-themed-loading>
|
||||
<ds-pagination
|
||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(searching$ | async)"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="pageInfoState$"
|
||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="epeople" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{labelPrefix + 'table.id' | translate}}</th>
|
||||
<th scope="col">{{labelPrefix + 'table.name' | translate}}</th>
|
||||
<th scope="col">{{labelPrefix + 'table.email' | translate}}</th>
|
||||
<th>{{labelPrefix + 'table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
|
||||
[ngClass]="{'table-primary' : isActive(epersonDto.eperson) | async}">
|
||||
<td>{{epersonDto.eperson.id}}</td>
|
||||
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
|
||||
<td>{{epersonDto.eperson.email}}</td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="toggleEditEPerson(epersonDto.eperson)"
|
||||
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
|
||||
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
<button [disabled]="!epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
|
||||
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
|
||||
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(pageInfoState$ | async)?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{labelPrefix + 'no-items' | translate}}
|
||||
</div>
|
||||
<div>
|
||||
<button (click)="clearFormAndResetResult();"
|
||||
class="search-button btn btn-secondary">{{labelPrefix + 'button.see-all' | translate}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-themed-loading *ngIf="searching$ | async"></ds-themed-loading>
|
||||
<ds-pagination
|
||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(searching$ | async)"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="pageInfoState$"
|
||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="epeople" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{labelPrefix + 'table.id' | translate}}</th>
|
||||
<th scope="col">{{labelPrefix + 'table.name' | translate}}</th>
|
||||
<th scope="col">{{labelPrefix + 'table.email' | translate}}</th>
|
||||
<th>{{labelPrefix + 'table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
|
||||
[ngClass]="{'table-primary' : isActive(epersonDto.eperson) | async}">
|
||||
<td>{{epersonDto.eperson.id}}</td>
|
||||
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
|
||||
<td>{{epersonDto.eperson.email}}</td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<button [routerLink]="getEditEPeoplePage(epersonDto.eperson.id)"
|
||||
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
|
||||
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
<button *ngIf="epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
|
||||
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
|
||||
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(pageInfoState$ | async)?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{labelPrefix + 'no-items' | translate}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -203,36 +203,6 @@ describe('EPeopleRegistryComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleEditEPerson', () => {
|
||||
describe('when you click on first edit eperson button', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
const editButtons = fixture.debugElement.queryAll(By.css('.access-control-editEPersonButton'));
|
||||
editButtons[0].triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('editEPerson form is toggled', () => {
|
||||
const ePeopleIds = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
ePersonDataServiceStub.getActiveEPerson().subscribe((activeEPerson: EPerson) => {
|
||||
if (ePeopleIds[0] && activeEPerson === ePeopleIds[0].nativeElement.textContent) {
|
||||
expect(component.isEPersonFormShown).toEqual(false);
|
||||
} else {
|
||||
expect(component.isEPersonFormShown).toEqual(true);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
it('EPerson search section is hidden', () => {
|
||||
expect(fixture.debugElement.query(By.css('#search'))).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteEPerson', () => {
|
||||
describe('when you click on first delete eperson button', () => {
|
||||
let ePeopleIdsFoundBeforeDelete;
|
||||
|
@@ -22,6 +22,7 @@ import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { NoContent } from '../../core/shared/NoContent.model';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
import { getEPersonEditRoute, getEPersonsRoute } from '../access-control-routing-paths';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-epeople-registry',
|
||||
@@ -64,11 +65,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
currentPage: 1
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether or not to show the EPerson form
|
||||
*/
|
||||
isEPersonFormShown: boolean;
|
||||
|
||||
// The search form
|
||||
searchForm;
|
||||
|
||||
@@ -114,17 +110,11 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
initialisePage() {
|
||||
this.searching$.next(true);
|
||||
this.isEPersonFormShown = false;
|
||||
this.search({scope: this.currentSearchScope, query: this.currentSearchQuery});
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
if (eperson != null && eperson.id) {
|
||||
this.isEPersonFormShown = true;
|
||||
}
|
||||
}));
|
||||
this.subs.push(this.ePeople$.pipe(
|
||||
switchMap((epeople: PaginatedList<EPerson>) => {
|
||||
if (epeople.pageInfo.totalElements > 0) {
|
||||
return combineLatest([...epeople.page.map((eperson: EPerson) => {
|
||||
return combineLatest(epeople.page.map((eperson: EPerson) => {
|
||||
return this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined).pipe(
|
||||
map((authorized) => {
|
||||
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
||||
@@ -133,7 +123,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
return epersonDtoModel;
|
||||
})
|
||||
);
|
||||
})]).pipe(map((dtos: EpersonDtoModel[]) => {
|
||||
})).pipe(map((dtos: EpersonDtoModel[]) => {
|
||||
return buildPaginatedList(epeople.pageInfo, dtos);
|
||||
}));
|
||||
} else {
|
||||
@@ -160,14 +150,14 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
const query: string = data.query;
|
||||
const scope: string = data.scope;
|
||||
if (query != null && this.currentSearchQuery !== query) {
|
||||
this.router.navigate([this.epersonService.getEPeoplePageRouterLink()], {
|
||||
void this.router.navigate([getEPersonsRoute()], {
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
this.currentSearchQuery = query;
|
||||
this.paginationService.resetPage(this.config.id);
|
||||
}
|
||||
if (scope != null && this.currentSearchScope !== scope) {
|
||||
this.router.navigate([this.epersonService.getEPeoplePageRouterLink()], {
|
||||
void this.router.navigate([getEPersonsRoute()], {
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
this.currentSearchScope = scope;
|
||||
@@ -205,23 +195,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
return this.epersonService.getActiveEPerson();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start editing the selected EPerson
|
||||
* @param ePerson
|
||||
*/
|
||||
toggleEditEPerson(ePerson: EPerson) {
|
||||
this.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => {
|
||||
if (ePerson === activeEPerson) {
|
||||
this.epersonService.cancelEditEPerson();
|
||||
this.isEPersonFormShown = false;
|
||||
} else {
|
||||
this.epersonService.editEPerson(ePerson);
|
||||
this.isEPersonFormShown = true;
|
||||
}
|
||||
});
|
||||
this.scrollToTop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes EPerson, show notification on success/failure & updates EPeople list
|
||||
*/
|
||||
@@ -242,7 +215,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
if (restResponse.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: this.dsoNameService.getName(ePerson)}));
|
||||
} else {
|
||||
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { id: ePerson.id, statusCode: restResponse.statusCode, errorMessage: restResponse.errorMessage }));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -264,16 +237,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
scrollToTop() {
|
||||
(function smoothscroll() {
|
||||
const currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
if (currentScroll > 0) {
|
||||
window.requestAnimationFrame(smoothscroll);
|
||||
window.scrollTo(0, currentScroll - (currentScroll / 8));
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all input-fields to be empty and search all search
|
||||
*/
|
||||
@@ -284,20 +247,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
this.search({query: ''});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will set everything to stale, which will cause the lists on this page to update.
|
||||
*/
|
||||
reset(): void {
|
||||
this.epersonService.getBrowseEndpoint().pipe(
|
||||
take(1),
|
||||
switchMap((href: string) => {
|
||||
return this.requestService.setStaleByHrefSubstring(href).pipe(
|
||||
take(1),
|
||||
);
|
||||
})
|
||||
).subscribe(()=>{
|
||||
this.epersonService.cancelEditEPerson();
|
||||
this.isEPersonFormShown = false;
|
||||
});
|
||||
getEditEPeoplePage(id: string): string {
|
||||
return getEPersonEditRoute(id);
|
||||
}
|
||||
}
|
||||
|
@@ -1,89 +1,97 @@
|
||||
<div *ngIf="epersonService.getActiveEPerson() | async; then editheader; else createHeader"></div>
|
||||
<div class="container">
|
||||
<div class="group-form row">
|
||||
<div class="col-12">
|
||||
|
||||
<ng-template #createHeader>
|
||||
<h4>{{messagePrefix + '.create' | translate}}</h4>
|
||||
</ng-template>
|
||||
<div *ngIf="epersonService.getActiveEPerson() | async; then editHeader; else createHeader"></div>
|
||||
|
||||
<ng-template #editheader>
|
||||
<h4>{{messagePrefix + '.edit' | translate}}</h4>
|
||||
</ng-template>
|
||||
<ng-template #createHeader>
|
||||
<h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1>
|
||||
</ng-template>
|
||||
|
||||
<ds-form [formId]="formId"
|
||||
[formModel]="formModel"
|
||||
[formGroup]="formGroup"
|
||||
[formLayout]="formLayout"
|
||||
[displayCancel]="false"
|
||||
[submitLabel]="submitLabel"
|
||||
(submitForm)="onSubmit()">
|
||||
<div before class="btn-group">
|
||||
<button (click)="onCancel()"
|
||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||
</div>
|
||||
<div *ngIf="displayResetPassword" between class="btn-group">
|
||||
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" (click)="resetPassword()">
|
||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
<div between class="btn-group ml-1">
|
||||
<button *ngIf="!isImpersonated" class="btn btn-primary" [ngClass]="{'d-none' : !(canImpersonate$ | async)}" (click)="impersonate()">
|
||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
|
||||
</button>
|
||||
<button *ngIf="isImpersonated" class="btn btn-primary" (click)="stopImpersonating()">
|
||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
<button after class="btn btn-danger delete-button" [disabled]="!(canDelete$ | async)" (click)="delete()">
|
||||
<i class="fas fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
|
||||
</button>
|
||||
</ds-form>
|
||||
<ng-template #editHeader>
|
||||
<h1 class="border-bottom pb-2">{{messagePrefix + '.edit' | translate}}</h1>
|
||||
</ng-template>
|
||||
|
||||
<ds-themed-loading [showMessage]="false" *ngIf="!formGroup"></ds-themed-loading>
|
||||
<ds-form [formId]="formId"
|
||||
[formModel]="formModel"
|
||||
[formGroup]="formGroup"
|
||||
[formLayout]="formLayout"
|
||||
[displayCancel]="false"
|
||||
[submitLabel]="submitLabel"
|
||||
(submitForm)="onSubmit()">
|
||||
<div before class="btn-group">
|
||||
<button (click)="onCancel()" type="button" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="displayResetPassword" between class="btn-group">
|
||||
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" type="button" (click)="resetPassword()">
|
||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="canImpersonate$ | async" between class="btn-group">
|
||||
<button *ngIf="!isImpersonated" class="btn btn-primary" type="button" (click)="impersonate()">
|
||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
|
||||
</button>
|
||||
<button *ngIf="isImpersonated" class="btn btn-primary" type="button" (click)="stopImpersonating()">
|
||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
<button *ngIf="canDelete$ | async" after class="btn btn-danger delete-button" type="button" (click)="delete()">
|
||||
<i class="fas fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
|
||||
</button>
|
||||
</ds-form>
|
||||
|
||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
||||
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5>
|
||||
<ds-themed-loading [showMessage]="false" *ngIf="!formGroup"></ds-themed-loading>
|
||||
|
||||
<ds-themed-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-themed-loading>
|
||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
||||
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(groups | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(groups | async)?.payload"
|
||||
[collectionSize]="(groups | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
<ds-themed-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-themed-loading>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="groups" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (groups | async)?.payload?.page">
|
||||
<td class="align-middle">{{group.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a (click)="groupsDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">
|
||||
{{ dsoNameService.getName(group) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">{{ dsoNameService.getName(undefined) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<ds-pagination
|
||||
*ngIf="(groups | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(groups | async)?.payload"
|
||||
[collectionSize]="(groups | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
|
||||
</ds-pagination>
|
||||
<div class="table-responsive">
|
||||
<table id="groups" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (groups | async)?.payload?.page">
|
||||
<td class="align-middle">{{group.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a (click)="groupsDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">
|
||||
{{ dsoNameService.getName(group) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">{{ dsoNameService.getName(undefined) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div *ngIf="(groups | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
<div>{{messagePrefix + '.memberOfNoGroups' | translate}}</div>
|
||||
<div>
|
||||
<button [routerLink]="[groupsDataService.getGroupRegistryRouterLink()]"
|
||||
class="btn btn-primary">{{messagePrefix + '.goToGroups' | translate}}</button>
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(groups | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
<div>{{messagePrefix + '.memberOfNoGroups' | translate}}</div>
|
||||
<div>
|
||||
<button [routerLink]="[groupsDataService.getGroupRegistryRouterLink()]"
|
||||
class="btn btn-primary">{{messagePrefix + '.goToGroups' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -31,6 +31,10 @@ import { PaginationServiceStub } from '../../../shared/testing/pagination-servic
|
||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
|
||||
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||
|
||||
describe('EPersonFormComponent', () => {
|
||||
let component: EPersonFormComponent;
|
||||
@@ -43,6 +47,8 @@ describe('EPersonFormComponent', () => {
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let groupsDataService: GroupDataService;
|
||||
let epersonRegistrationService: EpersonRegistrationService;
|
||||
let route: ActivatedRouteStub;
|
||||
let router: RouterStub;
|
||||
|
||||
let paginationService;
|
||||
|
||||
@@ -106,6 +112,9 @@ describe('EPersonFormComponent', () => {
|
||||
},
|
||||
getEPersonByEmail(email): Observable<RemoteData<EPerson>> {
|
||||
return createSuccessfulRemoteDataObject$(null);
|
||||
},
|
||||
findById(_id: string, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig<EPerson>[]): Observable<RemoteData<EPerson>> {
|
||||
return createSuccessfulRemoteDataObject$(null);
|
||||
}
|
||||
};
|
||||
builderService = Object.assign(getMockFormBuilderService(),{
|
||||
@@ -182,6 +191,8 @@ describe('EPersonFormComponent', () => {
|
||||
});
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
route = new ActivatedRouteStub();
|
||||
router = new RouterStub();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
@@ -202,6 +213,8 @@ describe('EPersonFormComponent', () => {
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])},
|
||||
{ provide: EpersonRegistrationService, useValue: epersonRegistrationService },
|
||||
{ provide: ActivatedRoute, useValue: route },
|
||||
{ provide: Router, useValue: router },
|
||||
EPeopleRegistryComponent
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -263,24 +276,18 @@ describe('EPersonFormComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
describe('firstName, lastName and email should be required', () => {
|
||||
it('form should be invalid because the firstName is required', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.formGroup.controls.firstName.valid).toBeFalse();
|
||||
expect(component.formGroup.controls.firstName.errors.required).toBeTrue();
|
||||
});
|
||||
}));
|
||||
it('form should be invalid because the lastName is required', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.formGroup.controls.lastName.valid).toBeFalse();
|
||||
expect(component.formGroup.controls.lastName.errors.required).toBeTrue();
|
||||
});
|
||||
}));
|
||||
it('form should be invalid because the email is required', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.formGroup.controls.email.valid).toBeFalse();
|
||||
expect(component.formGroup.controls.email.errors.required).toBeTrue();
|
||||
});
|
||||
}));
|
||||
it('form should be invalid because the firstName is required', () => {
|
||||
expect(component.formGroup.controls.firstName.valid).toBeFalse();
|
||||
expect(component.formGroup.controls.firstName.errors.required).toBeTrue();
|
||||
});
|
||||
it('form should be invalid because the lastName is required', () => {
|
||||
expect(component.formGroup.controls.lastName.valid).toBeFalse();
|
||||
expect(component.formGroup.controls.lastName.errors.required).toBeTrue();
|
||||
});
|
||||
it('form should be invalid because the email is required', () => {
|
||||
expect(component.formGroup.controls.email.valid).toBeFalse();
|
||||
expect(component.formGroup.controls.email.errors.required).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('after inserting information firstName,lastName and email not required', () => {
|
||||
@@ -290,24 +297,18 @@ describe('EPersonFormComponent', () => {
|
||||
component.formGroup.controls.email.setValue('test@test.com');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('firstName should be valid because the firstName is set', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
it('firstName should be valid because the firstName is set', () => {
|
||||
expect(component.formGroup.controls.firstName.valid).toBeTrue();
|
||||
expect(component.formGroup.controls.firstName.errors).toBeNull();
|
||||
});
|
||||
}));
|
||||
it('lastName should be valid because the lastName is set', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
});
|
||||
it('lastName should be valid because the lastName is set', () => {
|
||||
expect(component.formGroup.controls.lastName.valid).toBeTrue();
|
||||
expect(component.formGroup.controls.lastName.errors).toBeNull();
|
||||
});
|
||||
}));
|
||||
it('email should be valid because the email is set', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
});
|
||||
it('email should be valid because the email is set', () => {
|
||||
expect(component.formGroup.controls.email.valid).toBeTrue();
|
||||
expect(component.formGroup.controls.email.errors).toBeNull();
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -316,12 +317,10 @@ describe('EPersonFormComponent', () => {
|
||||
component.formGroup.controls.email.setValue('test@test');
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('email should not be valid because the email pattern', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
it('email should not be valid because the email pattern', () => {
|
||||
expect(component.formGroup.controls.email.valid).toBeFalse();
|
||||
expect(component.formGroup.controls.email.errors.pattern).toBeTruthy();
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('after already utilized email', () => {
|
||||
@@ -336,12 +335,10 @@ describe('EPersonFormComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('email should not be valid because email is already taken', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
it('email should not be valid because email is already taken', () => {
|
||||
expect(component.formGroup.controls.email.valid).toBeFalse();
|
||||
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -393,11 +390,9 @@ describe('EPersonFormComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit a new eperson using the correct values', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
}));
|
||||
it('should emit a new eperson using the correct values', () => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an active eperson', () => {
|
||||
@@ -428,11 +423,9 @@ describe('EPersonFormComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit the existing eperson using the correct values', waitForAsync(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
||||
});
|
||||
}));
|
||||
it('should emit the existing eperson using the correct values', () => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -491,16 +484,16 @@ describe('EPersonFormComponent', () => {
|
||||
|
||||
});
|
||||
|
||||
it('the delete button should be active if the eperson can be deleted', () => {
|
||||
it('the delete button should be visible if the ePerson can be deleted', () => {
|
||||
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
||||
expect(deleteButton.nativeElement.disabled).toBe(false);
|
||||
expect(deleteButton).not.toBeNull();
|
||||
});
|
||||
|
||||
it('the delete button should be disabled if the eperson cannot be deleted', () => {
|
||||
it('the delete button should be hidden if the ePerson cannot be deleted', () => {
|
||||
component.canDelete$ = observableOf(false);
|
||||
fixture.detectChanges();
|
||||
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
||||
expect(deleteButton.nativeElement.disabled).toBe(true);
|
||||
expect(deleteButton).toBeNull();
|
||||
});
|
||||
|
||||
it('should call the epersonFormComponent delete when clicked on the button', () => {
|
||||
|
@@ -38,6 +38,8 @@ import { Registration } from '../../../core/shared/registration.model';
|
||||
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
|
||||
import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component';
|
||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { getEPersonsRoute } from '../../access-control-routing-paths';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-eperson-form',
|
||||
@@ -194,6 +196,8 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
public requestService: RequestService,
|
||||
private epersonRegistrationService: EpersonRegistrationService,
|
||||
public dsoNameService: DSONameService,
|
||||
protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
) {
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
this.epersonInitial = eperson;
|
||||
@@ -213,7 +217,9 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
* This method will initialise the page
|
||||
*/
|
||||
initialisePage() {
|
||||
|
||||
this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData<EPerson>) => {
|
||||
this.epersonService.editEPerson(ePersonRD.payload);
|
||||
}));
|
||||
observableCombineLatest([
|
||||
this.translateService.get(`${this.messagePrefix}.firstName`),
|
||||
this.translateService.get(`${this.messagePrefix}.lastName`),
|
||||
@@ -339,6 +345,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
onCancel() {
|
||||
this.epersonService.cancelEditEPerson();
|
||||
this.cancelForm.emit();
|
||||
void this.router.navigate([getEPersonsRoute()]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -390,6 +397,8 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: this.dsoNameService.getName(ePersonToCreate) }));
|
||||
this.submitForm.emit(ePersonToCreate);
|
||||
this.epersonService.clearEPersonRequests();
|
||||
void this.router.navigateByUrl(getEPersonsRoute());
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: this.dsoNameService.getName(ePersonToCreate) }));
|
||||
this.cancelForm.emit();
|
||||
@@ -429,6 +438,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: this.dsoNameService.getName(editedEperson) }));
|
||||
this.submitForm.emit(editedEperson);
|
||||
void this.router.navigateByUrl(getEPersonsRoute());
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: this.dsoNameService.getName(editedEperson) }));
|
||||
this.cancelForm.emit();
|
||||
@@ -495,6 +505,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
).subscribe(({ restResponse, eperson }: { restResponse: RemoteData<NoContent> | null, eperson: EPerson }) => {
|
||||
if (restResponse?.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) }));
|
||||
void this.router.navigate([getEPersonsRoute()]);
|
||||
} else {
|
||||
this.notificationsService.error(`Error occurred when trying to delete EPerson with id: ${eperson?.id} with code: ${restResponse?.statusCode} and message: ${restResponse?.errorMessage}`);
|
||||
}
|
||||
@@ -541,16 +552,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will ensure that the page gets reset and that the cache is cleared
|
||||
*/
|
||||
reset() {
|
||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
|
||||
this.requestService.removeByHrefSubstring(eperson.self);
|
||||
});
|
||||
this.initialisePage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for the given ePerson if there is already an ePerson in the system with that email
|
||||
* and shows notification if this is the case
|
||||
|
@@ -0,0 +1,53 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||
import { ResolvedAction } from '../../core/resolving/resolver.actions';
|
||||
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
|
||||
export const EPERSON_EDIT_FOLLOW_LINKS: FollowLinkConfig<EPerson>[] = [
|
||||
followLink('groups'),
|
||||
];
|
||||
|
||||
/**
|
||||
* This class represents a resolver that requests a specific {@link EPerson} before the route is activated
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EPersonResolver implements Resolve<RemoteData<EPerson>> {
|
||||
|
||||
constructor(
|
||||
protected ePersonService: EPersonDataService,
|
||||
protected store: Store<any>,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for resolving a {@link EPerson} based on the parameters in the current route
|
||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||
* @returns `Observable<<RemoteData<EPerson>>` Emits the found {@link EPerson} based on the parameters in the current
|
||||
* route, or an error if something went wrong
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<EPerson>> {
|
||||
const ePersonRD$: Observable<RemoteData<EPerson>> = this.ePersonService.findById(route.params.id,
|
||||
true,
|
||||
false,
|
||||
...EPERSON_EDIT_FOLLOW_LINKS,
|
||||
).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
);
|
||||
|
||||
ePersonRD$.subscribe((ePersonRD: RemoteData<EPerson>) => {
|
||||
this.store.dispatch(new ResolvedAction(state.url, ePersonRD.payload));
|
||||
});
|
||||
|
||||
return ePersonRD$;
|
||||
}
|
||||
|
||||
}
|
@@ -2,14 +2,14 @@
|
||||
<div class="group-form row">
|
||||
<div class="col-12">
|
||||
|
||||
<div *ngIf="groupDataService.getActiveGroup() | async; then editheader; else createHeader"></div>
|
||||
<div *ngIf="groupDataService.getActiveGroup() | async; then editHeader; else createHeader"></div>
|
||||
|
||||
<ng-template #createHeader>
|
||||
<h2 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h2>
|
||||
<h1 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h1>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #editheader>
|
||||
<h2 class="border-bottom pb-2">
|
||||
<ng-template #editHeader>
|
||||
<h1 class="border-bottom pb-2">
|
||||
<span
|
||||
*dsContextHelp="{
|
||||
content: 'admin.access-control.groups.form.tooltip.editGroupPage',
|
||||
@@ -20,7 +20,7 @@
|
||||
>
|
||||
{{messagePrefix + '.head.edit' | translate}}
|
||||
</span>
|
||||
</h2>
|
||||
</h1>
|
||||
</ng-template>
|
||||
|
||||
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"
|
||||
@@ -36,12 +36,11 @@
|
||||
[displayCancel]="false"
|
||||
(submitForm)="onSubmit()">
|
||||
<div before class="btn-group">
|
||||
<button (click)="onCancel()"
|
||||
<button (click)="onCancel()" type="button"
|
||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||
</div>
|
||||
<div after *ngIf="groupBeingEdited != null" class="btn-group">
|
||||
<button class="btn btn-danger delete-button" [disabled]="!(canEdit$ | async) || groupBeingEdited.permanent"
|
||||
(click)="delete()">
|
||||
<div after *ngIf="(canEdit$ | async) && !groupBeingEdited?.permanent" class="btn-group">
|
||||
<button (click)="delete()" class="btn btn-danger delete-button" type="button">
|
||||
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -10,7 +10,6 @@ import {
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
ObservedValueOf,
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
@@ -37,7 +36,7 @@ import {
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteDataPayload
|
||||
} from '../../../core/shared/operators';
|
||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
||||
import { AlertType } from '../../../shared/alert/alert-type';
|
||||
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
|
||||
import { hasValue, isNotEmpty, hasValueOperator } from '../../../shared/empty.util';
|
||||
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||
@@ -48,6 +47,7 @@ import { Operation } from 'fast-json-patch';
|
||||
import { ValidateGroupExists } from './validators/group-exists.validator';
|
||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
import { getGroupEditRoute, getGroupsRoute } from '../../access-control-routing-paths';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-group-form',
|
||||
@@ -165,19 +165,19 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
|
||||
hasValueOperator(),
|
||||
switchMap((group: Group) => {
|
||||
return observableCombineLatest(
|
||||
return observableCombineLatest([
|
||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined),
|
||||
this.hasLinkedDSO(group),
|
||||
(isAuthorized: ObservedValueOf<Observable<boolean>>, hasLinkedDSO: ObservedValueOf<Observable<boolean>>) => {
|
||||
return isAuthorized && !hasLinkedDSO;
|
||||
});
|
||||
})
|
||||
]).pipe(
|
||||
map(([isAuthorized, hasLinkedDSO]: [boolean, boolean]) => isAuthorized && !hasLinkedDSO),
|
||||
);
|
||||
}),
|
||||
);
|
||||
observableCombineLatest(
|
||||
observableCombineLatest([
|
||||
this.translateService.get(`${this.messagePrefix}.groupName`),
|
||||
this.translateService.get(`${this.messagePrefix}.groupCommunity`),
|
||||
this.translateService.get(`${this.messagePrefix}.groupDescription`)
|
||||
).subscribe(([groupName, groupCommunity, groupDescription]) => {
|
||||
]).subscribe(([groupName, groupCommunity, groupDescription]) => {
|
||||
this.groupName = new DynamicInputModel({
|
||||
id: 'groupName',
|
||||
label: groupName,
|
||||
@@ -215,12 +215,12 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.subs.push(
|
||||
observableCombineLatest(
|
||||
observableCombineLatest([
|
||||
this.groupDataService.getActiveGroup(),
|
||||
this.canEdit$,
|
||||
this.groupDataService.getActiveGroup()
|
||||
.pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload())))
|
||||
).subscribe(([activeGroup, canEdit, linkedObject]) => {
|
||||
]).subscribe(([activeGroup, canEdit, linkedObject]) => {
|
||||
|
||||
if (activeGroup != null) {
|
||||
|
||||
@@ -230,12 +230,14 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
this.groupBeingEdited = activeGroup;
|
||||
|
||||
if (linkedObject?.name) {
|
||||
this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity);
|
||||
this.formGroup.patchValue({
|
||||
groupName: activeGroup.name,
|
||||
groupCommunity: linkedObject?.name ?? '',
|
||||
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
||||
});
|
||||
if (!this.formGroup.controls.groupCommunity) {
|
||||
this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity);
|
||||
this.formGroup.patchValue({
|
||||
groupName: activeGroup.name,
|
||||
groupCommunity: linkedObject?.name ?? '',
|
||||
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.formModel = [
|
||||
this.groupName,
|
||||
@@ -263,7 +265,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
onCancel() {
|
||||
this.groupDataService.cancelEditGroup();
|
||||
this.cancelForm.emit();
|
||||
this.router.navigate([this.groupDataService.getGroupRegistryRouterLink()]);
|
||||
void this.router.navigate([getGroupsRoute()]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -310,7 +312,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
const groupSelfLink = rd.payload._links.self.href;
|
||||
this.setActiveGroupWithLink(groupSelfLink);
|
||||
this.groupDataService.clearGroupsRequests();
|
||||
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLinkWithID(rd.payload.uuid));
|
||||
void this.router.navigateByUrl(getGroupEditRoute(rd.payload.uuid));
|
||||
}
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.created.failure', { name: groupToCreate.name }));
|
||||
|
@@ -1,110 +1,12 @@
|
||||
<ng-container>
|
||||
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
||||
<h2 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h2>
|
||||
|
||||
<h4 id="search" class="border-bottom pb-2">
|
||||
<span
|
||||
*dsContextHelp="{
|
||||
content: 'admin.access-control.groups.form.tooltip.editGroup.addEpeople',
|
||||
id: 'edit-group-add-epeople',
|
||||
iconPlacement: 'right',
|
||||
tooltipPlacement: ['top', 'right', 'bottom']
|
||||
}"
|
||||
>
|
||||
{{messagePrefix + '.search.head' | translate}}
|
||||
</span>
|
||||
</h4>
|
||||
<h3>{{messagePrefix + '.headMembers' | translate}}</h3>
|
||||
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||
<div>
|
||||
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
||||
<option value="metadata">{{messagePrefix + '.search.scope.metadata' | translate}}</option>
|
||||
<option value="email">{{messagePrefix + '.search.scope.email' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-grow-1 mr-3 ml-3">
|
||||
<div class="form-group input-group">
|
||||
<input type="text" name="query" id="query" formControlName="query"
|
||||
class="form-control" aria-label="Search input">
|
||||
<span class="input-group-append">
|
||||
<button type="submit" class="search-button btn btn-primary">
|
||||
<i class="fas fa-search"></i> {{ messagePrefix + '.search.button' | translate }}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button (click)="clearFormAndResetResult();"
|
||||
class="btn btn-secondary">{{messagePrefix + '.button.see-all' | translate}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-pagination *ngIf="(ePeopleSearchDtos | async)?.totalElements > 0"
|
||||
[paginationOptions]="configSearch"
|
||||
[pageInfoState]="(ePeopleSearchDtos | async)"
|
||||
[collectionSize]="(ePeopleSearchDtos | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="epersonsSearch" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.identity' | translate}}</th>
|
||||
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let ePerson of (ePeopleSearchDtos | async)?.page">
|
||||
<td class="align-middle">{{ePerson.eperson.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">
|
||||
{{ dsoNameService.getName(ePerson.eperson) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}<br/>
|
||||
{{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button *ngIf="ePerson.memberOfGroup"
|
||||
(click)="deleteMemberFromGroup(ePerson)"
|
||||
[disabled]="actionConfig.remove.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
||||
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||
</button>
|
||||
|
||||
<button *ngIf="!ePerson.memberOfGroup"
|
||||
(click)="addMemberToGroup(ePerson)"
|
||||
[disabled]="actionConfig.add.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
||||
<i [ngClass]="actionConfig.add.icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(ePeopleSearchDtos | async)?.totalElements == 0 && searchDone"
|
||||
class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-items' | translate}}
|
||||
</div>
|
||||
|
||||
<h4>{{messagePrefix + '.headMembers' | translate}}</h4>
|
||||
|
||||
<ds-pagination *ngIf="(ePeopleMembersOfGroupDtos | async)?.totalElements > 0"
|
||||
<ds-pagination *ngIf="(ePeopleMembersOfGroup | async)?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(ePeopleMembersOfGroupDtos | async)"
|
||||
[collectionSize]="(ePeopleMembersOfGroupDtos | async)?.totalElements"
|
||||
[pageInfoState]="(ePeopleMembersOfGroup | async)"
|
||||
[collectionSize]="(ePeopleMembersOfGroup | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
@@ -119,32 +21,106 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let ePerson of (ePeopleMembersOfGroupDtos | async)?.page">
|
||||
<td class="align-middle">{{ePerson.eperson.id}}</td>
|
||||
<tr *ngFor="let eperson of (ePeopleMembersOfGroup | async)?.page">
|
||||
<td class="align-middle">{{eperson.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
|
||||
<a (click)="ePersonDataService.startEditingNewEPerson(eperson)"
|
||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">
|
||||
{{ dsoNameService.getName(ePerson.eperson) }}
|
||||
{{ dsoNameService.getName(eperson) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}<br/>
|
||||
{{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }}
|
||||
{{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}<br/>
|
||||
{{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button *ngIf="ePerson.memberOfGroup"
|
||||
(click)="deleteMemberFromGroup(ePerson)"
|
||||
<button (click)="deleteMemberFromGroup(eperson)"
|
||||
[disabled]="actionConfig.remove.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(eperson) } }}">
|
||||
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||
</button>
|
||||
<button *ngIf="!ePerson.memberOfGroup"
|
||||
(click)="addMemberToGroup(ePerson)"
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(ePeopleMembersOfGroup | async) == undefined || (ePeopleMembersOfGroup | async)?.totalElements == 0" class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-members-yet' | translate}}
|
||||
</div>
|
||||
|
||||
<h3 id="search" class="border-bottom pb-2">
|
||||
<span
|
||||
*dsContextHelp="{
|
||||
content: 'admin.access-control.groups.form.tooltip.editGroup.addEpeople',
|
||||
id: 'edit-group-add-epeople',
|
||||
iconPlacement: 'right',
|
||||
tooltipPlacement: ['top', 'right', 'bottom']
|
||||
}"
|
||||
>
|
||||
{{messagePrefix + '.search.head' | translate}}
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||
<div class="flex-grow-1 mr-3">
|
||||
<div class="form-group input-group mr-3">
|
||||
<input type="text" name="query" id="query" formControlName="query"
|
||||
class="form-control" aria-label="Search input">
|
||||
<span class="input-group-append">
|
||||
<button type="submit" class="search-button btn btn-primary">
|
||||
<i class="fas fa-search"></i> {{ messagePrefix + '.search.button' | translate }}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button (click)="clearFormAndResetResult();"
|
||||
class="btn btn-secondary">{{messagePrefix + '.button.see-all' | translate}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-pagination *ngIf="(ePeopleSearch | async)?.totalElements > 0"
|
||||
[paginationOptions]="configSearch"
|
||||
[pageInfoState]="(ePeopleSearch | async)"
|
||||
[collectionSize]="(ePeopleSearch | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="epersonsSearch" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.identity' | translate}}</th>
|
||||
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let eperson of (ePeopleSearch | async)?.page">
|
||||
<td class="align-middle">{{eperson.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a (click)="ePersonDataService.startEditingNewEPerson(eperson)"
|
||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">
|
||||
{{ dsoNameService.getName(eperson) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}<br/>
|
||||
{{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="addMemberToGroup(eperson)"
|
||||
[disabled]="actionConfig.add.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}">
|
||||
<i [ngClass]="actionConfig.add.icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -156,9 +132,10 @@
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(ePeopleMembersOfGroupDtos | async) == undefined || (ePeopleMembersOfGroupDtos | async)?.totalElements == 0" class="alert alert-info w-100 mb-2"
|
||||
<div *ngIf="(ePeopleSearch | async)?.totalElements == 0 && searchDone"
|
||||
class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-members-yet' | translate}}
|
||||
{{messagePrefix + '.no-items' | translate}}
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
@@ -17,7 +17,7 @@ import { Group } from '../../../../core/eperson/models/group.model';
|
||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock';
|
||||
import { GroupMock } from '../../../../shared/testing/group-mock';
|
||||
import { MembersListComponent } from './members-list.component';
|
||||
import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson.mock';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||
@@ -39,28 +39,26 @@ describe('MembersListComponent', () => {
|
||||
let ePersonDataServiceStub: any;
|
||||
let groupsDataServiceStub: any;
|
||||
let activeGroup;
|
||||
let allEPersons: EPerson[];
|
||||
let allGroups: Group[];
|
||||
let epersonMembers: EPerson[];
|
||||
let subgroupMembers: Group[];
|
||||
let epersonNonMembers: EPerson[];
|
||||
let paginationService;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
activeGroup = GroupMock;
|
||||
epersonMembers = [EPersonMock2];
|
||||
subgroupMembers = [GroupMock2];
|
||||
allEPersons = [EPersonMock, EPersonMock2];
|
||||
allGroups = [GroupMock, GroupMock2];
|
||||
epersonNonMembers = [EPersonMock];
|
||||
ePersonDataServiceStub = {
|
||||
activeGroup: activeGroup,
|
||||
epersonMembers: epersonMembers,
|
||||
subgroupMembers: subgroupMembers,
|
||||
epersonNonMembers: epersonNonMembers,
|
||||
// This method is used to get all the current members
|
||||
findListByHref(_href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
|
||||
},
|
||||
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
// This method is used to search across *non-members*
|
||||
searchNonMembers(query: string, group: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
if (query === '') {
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), allEPersons));
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), epersonNonMembers));
|
||||
}
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));
|
||||
},
|
||||
@@ -77,22 +75,22 @@ describe('MembersListComponent', () => {
|
||||
groupsDataServiceStub = {
|
||||
activeGroup: activeGroup,
|
||||
epersonMembers: epersonMembers,
|
||||
subgroupMembers: subgroupMembers,
|
||||
allGroups: allGroups,
|
||||
epersonNonMembers: epersonNonMembers,
|
||||
getActiveGroup(): Observable<Group> {
|
||||
return observableOf(activeGroup);
|
||||
},
|
||||
getEPersonMembers() {
|
||||
return this.epersonMembers;
|
||||
},
|
||||
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
if (query === '') {
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), this.allGroups));
|
||||
}
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));
|
||||
},
|
||||
addMemberToGroup(parentGroup, eperson: EPerson): Observable<RestResponse> {
|
||||
this.epersonMembers = [...this.epersonMembers, eperson];
|
||||
addMemberToGroup(parentGroup, epersonToAdd: EPerson): Observable<RestResponse> {
|
||||
// Add eperson to list of members
|
||||
this.epersonMembers = [...this.epersonMembers, epersonToAdd];
|
||||
// Remove eperson from list of non-members
|
||||
this.epersonNonMembers.forEach( (eperson: EPerson, index: number) => {
|
||||
if (eperson.id === epersonToAdd.id) {
|
||||
this.epersonNonMembers.splice(index, 1);
|
||||
}
|
||||
});
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
},
|
||||
clearGroupsRequests() {
|
||||
@@ -105,14 +103,14 @@ describe('MembersListComponent', () => {
|
||||
return '/access-control/groups/' + group.id;
|
||||
},
|
||||
deleteMemberFromGroup(parentGroup, epersonToDelete: EPerson): Observable<RestResponse> {
|
||||
this.epersonMembers = this.epersonMembers.find((eperson: EPerson) => {
|
||||
if (eperson.id !== epersonToDelete.id) {
|
||||
return eperson;
|
||||
// Remove eperson from list of members
|
||||
this.epersonMembers.forEach( (eperson: EPerson, index: number) => {
|
||||
if (eperson.id === epersonToDelete.id) {
|
||||
this.epersonMembers.splice(index, 1);
|
||||
}
|
||||
});
|
||||
if (this.epersonMembers === undefined) {
|
||||
this.epersonMembers = [];
|
||||
}
|
||||
// Add eperson to list of non-members
|
||||
this.epersonNonMembers = [...this.epersonNonMembers, epersonToDelete];
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
}
|
||||
};
|
||||
@@ -160,13 +158,37 @@ describe('MembersListComponent', () => {
|
||||
expect(comp).toBeDefined();
|
||||
}));
|
||||
|
||||
it('should show list of eperson members of current active group', () => {
|
||||
const epersonIdsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tr td:first-child'));
|
||||
expect(epersonIdsFound.length).toEqual(1);
|
||||
epersonMembers.map((eperson: EPerson) => {
|
||||
expect(epersonIdsFound.find((foundEl) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === eperson.uuid);
|
||||
})).toBeTruthy();
|
||||
describe('current members list', () => {
|
||||
it('should show list of eperson members of current active group', () => {
|
||||
const epersonIdsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tr td:first-child'));
|
||||
expect(epersonIdsFound.length).toEqual(1);
|
||||
epersonMembers.map((eperson: EPerson) => {
|
||||
expect(epersonIdsFound.find((foundEl) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === eperson.uuid);
|
||||
})).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show a delete button next to each member', () => {
|
||||
const epersonsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tbody tr'));
|
||||
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
||||
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
||||
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||
expect(addButton).toBeNull();
|
||||
expect(deleteButton).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if first delete button is pressed', () => {
|
||||
beforeEach(() => {
|
||||
const deleteButton: DebugElement = fixture.debugElement.query(By.css('#ePeopleMembersOfGroup tbody .fa-trash-alt'));
|
||||
deleteButton.nativeElement.click();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('then no ePerson remains as a member of the active group.', () => {
|
||||
const epersonsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tbody tr'));
|
||||
expect(epersonsFound.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -174,76 +196,40 @@ describe('MembersListComponent', () => {
|
||||
describe('when searching without query', () => {
|
||||
let epersonsFound: DebugElement[];
|
||||
beforeEach(fakeAsync(() => {
|
||||
spyOn(component, 'isMemberOfGroup').and.callFake((ePerson: EPerson) => {
|
||||
return observableOf(activeGroup.epersons.includes(ePerson));
|
||||
});
|
||||
component.search({ scope: 'metadata', query: '' });
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||
// Stop using the fake spy function (because otherwise the clicking on the buttons will not change anything
|
||||
// because they don't change the value of activeGroup.epersons)
|
||||
jasmine.getEnv().allowRespy(true);
|
||||
spyOn(component, 'isMemberOfGroup').and.callThrough();
|
||||
}));
|
||||
|
||||
it('should display all epersons', () => {
|
||||
expect(epersonsFound.length).toEqual(2);
|
||||
it('should display only non-members of the group', () => {
|
||||
const epersonIdsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr td:first-child'));
|
||||
expect(epersonIdsFound.length).toEqual(1);
|
||||
epersonNonMembers.map((eperson: EPerson) => {
|
||||
expect(epersonIdsFound.find((foundEl) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === eperson.uuid);
|
||||
})).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if eperson is already a eperson', () => {
|
||||
it('should have delete button, else it should have add button', () => {
|
||||
const memberIds: string[] = activeGroup.epersons.map((ePerson: EPerson) => ePerson.id);
|
||||
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
||||
const epersonId: DebugElement = foundEPersonRowElement.query(By.css('td:first-child'));
|
||||
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
||||
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||
if (memberIds.includes(epersonId.nativeElement.textContent)) {
|
||||
expect(addButton).toBeNull();
|
||||
expect(deleteButton).not.toBeNull();
|
||||
} else {
|
||||
expect(deleteButton).toBeNull();
|
||||
expect(addButton).not.toBeNull();
|
||||
}
|
||||
});
|
||||
it('should display an add button next to non-members, not a delete button', () => {
|
||||
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
||||
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
||||
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||
expect(addButton).not.toBeNull();
|
||||
expect(deleteButton).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if first add button is pressed', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
beforeEach(() => {
|
||||
const addButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
|
||||
addButton.nativeElement.click();
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('then all the ePersons are member of the active group', () => {
|
||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||
expect(epersonsFound.length).toEqual(2);
|
||||
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
||||
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
||||
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||
expect(addButton).toBeNull();
|
||||
expect(deleteButton).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('if first delete button is pressed', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
const deleteButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt'));
|
||||
deleteButton.nativeElement.click();
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('then no ePerson is member of the active group', () => {
|
||||
it('then all (two) ePersons are member of the active group. No non-members left', () => {
|
||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||
expect(epersonsFound.length).toEqual(2);
|
||||
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
||||
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
||||
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||
expect(deleteButton).toBeNull();
|
||||
expect(addButton).not.toBeNull();
|
||||
});
|
||||
expect(epersonsFound.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -4,28 +4,23 @@ import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
Subscription,
|
||||
BehaviorSubject,
|
||||
combineLatest as observableCombineLatest,
|
||||
ObservedValueOf,
|
||||
BehaviorSubject
|
||||
} from 'rxjs';
|
||||
import { defaultIfEmpty, map, mergeMap, switchMap, take } from 'rxjs/operators';
|
||||
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||
import { Group } from '../../../../core/eperson/models/group.model';
|
||||
import {
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstCompletedRemoteData,
|
||||
getAllCompletedRemoteData,
|
||||
getRemoteDataPayload
|
||||
} from '../../../../core/shared/operators';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||
import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model';
|
||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@@ -34,8 +29,8 @@ import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||
*/
|
||||
enum SubKey {
|
||||
ActiveGroup,
|
||||
MembersDTO,
|
||||
SearchResultsDTO,
|
||||
Members,
|
||||
SearchResults,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,11 +91,11 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* EPeople being displayed in search result, initially all members, after search result of search
|
||||
*/
|
||||
ePeopleSearchDtos: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>(undefined);
|
||||
ePeopleSearch: BehaviorSubject<PaginatedList<EPerson>> = new BehaviorSubject<PaginatedList<EPerson>>(undefined);
|
||||
/**
|
||||
* List of EPeople members of currently active group being edited
|
||||
*/
|
||||
ePeopleMembersOfGroupDtos: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>(undefined);
|
||||
ePeopleMembersOfGroup: BehaviorSubject<PaginatedList<EPerson>> = new BehaviorSubject<PaginatedList<EPerson>>(undefined);
|
||||
|
||||
/**
|
||||
* Pagination config used to display the list of EPeople that are result of EPeople search
|
||||
@@ -129,7 +124,6 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Current search in edit group - epeople search form
|
||||
currentSearchQuery: string;
|
||||
currentSearchScope: string;
|
||||
|
||||
// Whether or not user has done a EPeople search yet
|
||||
searchDone: boolean;
|
||||
@@ -148,18 +142,17 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
public dsoNameService: DSONameService,
|
||||
) {
|
||||
this.currentSearchQuery = '';
|
||||
this.currentSearchScope = 'metadata';
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.searchForm = this.formBuilder.group(({
|
||||
scope: 'metadata',
|
||||
query: '',
|
||||
}));
|
||||
this.subs.set(SubKey.ActiveGroup, this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
this.groupBeingEdited = activeGroup;
|
||||
this.retrieveMembers(this.config.currentPage);
|
||||
this.search({query: ''});
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -171,8 +164,8 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
* @private
|
||||
*/
|
||||
retrieveMembers(page: number): void {
|
||||
this.unsubFrom(SubKey.MembersDTO);
|
||||
this.subs.set(SubKey.MembersDTO,
|
||||
this.unsubFrom(SubKey.Members);
|
||||
this.subs.set(SubKey.Members,
|
||||
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||
switchMap((currentPagination) => {
|
||||
return this.ePersonDataService.findListByHref(this.groupBeingEdited._links.epersons.href, {
|
||||
@@ -189,49 +182,12 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
return rd;
|
||||
}
|
||||
}),
|
||||
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
||||
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
|
||||
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
||||
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
||||
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
||||
epersonDtoModel.eperson = member;
|
||||
epersonDtoModel.memberOfGroup = isMember;
|
||||
return epersonDtoModel;
|
||||
});
|
||||
return dto$;
|
||||
})]);
|
||||
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
|
||||
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
||||
}));
|
||||
}))
|
||||
.subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
|
||||
this.ePeopleMembersOfGroupDtos.next(paginatedListOfDTOs);
|
||||
getRemoteDataPayload())
|
||||
.subscribe((paginatedListOfEPersons: PaginatedList<EPerson>) => {
|
||||
this.ePeopleMembersOfGroup.next(paginatedListOfEPersons);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the given ePerson is a member of the group currently being edited
|
||||
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
|
||||
*/
|
||||
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
|
||||
return this.groupDataService.getActiveGroup().pipe(take(1),
|
||||
mergeMap((group: Group) => {
|
||||
if (group != null) {
|
||||
return this.ePersonDataService.findListByHref(group._links.epersons.href, {
|
||||
currentPage: 1,
|
||||
elementsPerPage: 9999
|
||||
})
|
||||
.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((listEPeopleInGroup: PaginatedList<EPerson>) => listEPeopleInGroup.page.filter((ePersonInList: EPerson) => ePersonInList.id === possibleMember.id)),
|
||||
map((epeople: EPerson[]) => epeople.length > 0));
|
||||
} else {
|
||||
return observableOf(false);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from a subscription if it's still subscribed, and remove it from the map of
|
||||
* active subscriptions
|
||||
@@ -248,14 +204,18 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* Deletes a given EPerson from the members list of the group currently being edited
|
||||
* @param ePerson EPerson we want to delete as member from group that is currently being edited
|
||||
* @param eperson EPerson we want to delete as member from group that is currently being edited
|
||||
*/
|
||||
deleteMemberFromGroup(ePerson: EpersonDtoModel) {
|
||||
ePerson.memberOfGroup = false;
|
||||
deleteMemberFromGroup(eperson: EPerson) {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
|
||||
this.showNotifications('deleteMember', response, this.dsoNameService.getName(ePerson.eperson), activeGroup);
|
||||
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, eperson);
|
||||
this.showNotifications('deleteMember', response, this.dsoNameService.getName(eperson), activeGroup);
|
||||
// Reload search results (if there is an active query).
|
||||
// This will potentially add this deleted subgroup into the list of search results.
|
||||
if (this.currentSearchQuery != null) {
|
||||
this.search({query: this.currentSearchQuery});
|
||||
}
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||
}
|
||||
@@ -264,14 +224,18 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* Adds a given EPerson to the members list of the group currently being edited
|
||||
* @param ePerson EPerson we want to add as member to group that is currently being edited
|
||||
* @param eperson EPerson we want to add as member to group that is currently being edited
|
||||
*/
|
||||
addMemberToGroup(ePerson: EpersonDtoModel) {
|
||||
ePerson.memberOfGroup = true;
|
||||
addMemberToGroup(eperson: EPerson) {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson.eperson);
|
||||
this.showNotifications('addMember', response, this.dsoNameService.getName(ePerson.eperson), activeGroup);
|
||||
const response = this.groupDataService.addMemberToGroup(activeGroup, eperson);
|
||||
this.showNotifications('addMember', response, this.dsoNameService.getName(eperson), activeGroup);
|
||||
// Reload search results (if there is an active query).
|
||||
// This will potentially add this deleted subgroup into the list of search results.
|
||||
if (this.currentSearchQuery != null) {
|
||||
this.search({query: this.currentSearchQuery});
|
||||
}
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||
}
|
||||
@@ -279,37 +243,25 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Search in the EPeople by name, email or metadata
|
||||
* @param data Contains scope and query param
|
||||
* Search all EPeople who are NOT a member of the current group by name, email or metadata
|
||||
* @param data Contains query param
|
||||
*/
|
||||
search(data: any) {
|
||||
this.unsubFrom(SubKey.SearchResultsDTO);
|
||||
this.subs.set(SubKey.SearchResultsDTO,
|
||||
this.unsubFrom(SubKey.SearchResults);
|
||||
this.subs.set(SubKey.SearchResults,
|
||||
this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe(
|
||||
switchMap((paginationOptions) => {
|
||||
|
||||
const query: string = data.query;
|
||||
const scope: string = data.scope;
|
||||
if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) {
|
||||
this.router.navigate([], {
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
this.currentSearchQuery = query;
|
||||
this.paginationService.resetPage(this.configSearch.id);
|
||||
}
|
||||
if (scope != null && this.currentSearchScope !== scope && this.groupBeingEdited) {
|
||||
this.router.navigate([], {
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
this.currentSearchScope = scope;
|
||||
this.paginationService.resetPage(this.configSearch.id);
|
||||
}
|
||||
this.searchDone = true;
|
||||
|
||||
return this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
|
||||
return this.ePersonDataService.searchNonMembers(this.currentSearchQuery, this.groupBeingEdited.id, {
|
||||
currentPage: paginationOptions.currentPage,
|
||||
elementsPerPage: paginationOptions.pageSize
|
||||
});
|
||||
}, false, true);
|
||||
}),
|
||||
getAllCompletedRemoteData(),
|
||||
map((rd: RemoteData<any>) => {
|
||||
@@ -319,23 +271,9 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
return rd;
|
||||
}
|
||||
}),
|
||||
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
||||
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
|
||||
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
||||
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
||||
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
||||
epersonDtoModel.eperson = member;
|
||||
epersonDtoModel.memberOfGroup = isMember;
|
||||
return epersonDtoModel;
|
||||
});
|
||||
return dto$;
|
||||
})]);
|
||||
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
|
||||
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
||||
}));
|
||||
}))
|
||||
.subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
|
||||
this.ePeopleSearchDtos.next(paginatedListOfDTOs);
|
||||
getRemoteDataPayload())
|
||||
.subscribe((paginatedListOfEPersons: PaginatedList<EPerson>) => {
|
||||
this.ePeopleSearch.next(paginatedListOfEPersons);
|
||||
}));
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,55 @@
|
||||
<ng-container>
|
||||
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
||||
|
||||
<h4>{{messagePrefix + '.headSubgroups' | translate}}</h4>
|
||||
|
||||
<ds-pagination *ngIf="(subGroups$ | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(subGroups$ | async)?.payload"
|
||||
[collectionSize]="(subGroups$ | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="subgroupsOfGroup" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (subGroups$ | async)?.payload?.page">
|
||||
<td class="align-middle">{{group.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a (click)="groupDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">
|
||||
{{ dsoNameService.getName(group) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload)}}</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="deleteSubgroupFromGroup(group)"
|
||||
class="btn btn-outline-danger btn-sm deleteButton"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(group) } }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(subGroups$ | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-subgroups-yet' | translate}}
|
||||
</div>
|
||||
|
||||
<h4 id="search" class="border-bottom pb-2">
|
||||
<span *dsContextHelp="{
|
||||
content: 'admin.access-control.groups.form.tooltip.editGroup.addSubgroups',
|
||||
@@ -62,17 +111,7 @@
|
||||
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload) }}</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button *ngIf="(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
||||
(click)="deleteSubgroupFromGroup(group)"
|
||||
class="btn btn-outline-danger btn-sm deleteButton"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(group) } }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
|
||||
<span *ngIf="(isActiveGroup(group) | async)">{{ messagePrefix + '.table.edit.currentGroup' | translate }}</span>
|
||||
|
||||
<button *ngIf="!(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
||||
(click)="addSubgroupToGroup(group)"
|
||||
<button (click)="addSubgroupToGroup(group)"
|
||||
class="btn btn-outline-primary btn-sm addButton"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(group) } }}">
|
||||
<i class="fas fa-plus fa-fw"></i>
|
||||
@@ -90,53 +129,4 @@
|
||||
{{messagePrefix + '.no-items' | translate}}
|
||||
</div>
|
||||
|
||||
<h4>{{messagePrefix + '.headSubgroups' | translate}}</h4>
|
||||
|
||||
<ds-pagination *ngIf="(subGroups$ | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(subGroups$ | async)?.payload"
|
||||
[collectionSize]="(subGroups$ | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="subgroupsOfGroup" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
|
||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (subGroups$ | async)?.payload?.page">
|
||||
<td class="align-middle">{{group.id}}</td>
|
||||
<td class="align-middle">
|
||||
<a (click)="groupDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">
|
||||
{{ dsoNameService.getName(group) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload)}}</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="deleteSubgroupFromGroup(group)"
|
||||
class="btn btn-outline-danger btn-sm deleteButton"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(group) } }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(subGroups$ | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-subgroups-yet' | translate}}
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
|
||||
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||
import { ComponentFixture, fakeAsync, flush, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { BrowserModule, By } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { Observable, of as observableOf, BehaviorSubject } from 'rxjs';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { RestResponse } from '../../../../core/cache/response.models';
|
||||
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
@@ -18,19 +18,18 @@ import { NotificationsService } from '../../../../shared/notifications/notificat
|
||||
import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock';
|
||||
import { SubgroupsListComponent } from './subgroups-list.component';
|
||||
import {
|
||||
createSuccessfulRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../../../shared/remote-data.utils';
|
||||
import { RouterMock } from '../../../../shared/mocks/router.mock';
|
||||
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
|
||||
import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
|
||||
import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
|
||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock';
|
||||
import { EPersonMock2 } from 'src/app/shared/testing/eperson.mock';
|
||||
|
||||
describe('SubgroupsListComponent', () => {
|
||||
let component: SubgroupsListComponent;
|
||||
@@ -39,44 +38,70 @@ describe('SubgroupsListComponent', () => {
|
||||
let builderService: FormBuilderService;
|
||||
let ePersonDataServiceStub: any;
|
||||
let groupsDataServiceStub: any;
|
||||
let activeGroup;
|
||||
let activeGroup: Group;
|
||||
let subgroups: Group[];
|
||||
let allGroups: Group[];
|
||||
let groupNonMembers: Group[];
|
||||
let routerStub;
|
||||
let paginationService;
|
||||
// Define a new mock activegroup for all tests below
|
||||
let mockActiveGroup: Group = Object.assign(new Group(), {
|
||||
handle: null,
|
||||
subgroups: [GroupMock2],
|
||||
epersons: [EPersonMock2],
|
||||
selfRegistered: false,
|
||||
permanent: false,
|
||||
_links: {
|
||||
self: {
|
||||
href: 'https://rest.api/server/api/eperson/groups/activegroupid',
|
||||
},
|
||||
subgroups: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/subgroups' },
|
||||
object: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/object' },
|
||||
epersons: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/epersons' }
|
||||
},
|
||||
_name: 'activegroupname',
|
||||
id: 'activegroupid',
|
||||
uuid: 'activegroupid',
|
||||
type: 'group',
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
activeGroup = GroupMock;
|
||||
activeGroup = mockActiveGroup;
|
||||
subgroups = [GroupMock2];
|
||||
allGroups = [GroupMock, GroupMock2];
|
||||
groupNonMembers = [GroupMock];
|
||||
ePersonDataServiceStub = {};
|
||||
groupsDataServiceStub = {
|
||||
activeGroup: activeGroup,
|
||||
subgroups$: new BehaviorSubject(subgroups),
|
||||
subgroups: subgroups,
|
||||
groupNonMembers: groupNonMembers,
|
||||
getActiveGroup(): Observable<Group> {
|
||||
return observableOf(this.activeGroup);
|
||||
},
|
||||
getSubgroups(): Group {
|
||||
return this.activeGroup;
|
||||
return this.subgroups;
|
||||
},
|
||||
// This method is used to get all the current subgroups
|
||||
findListByHref(_href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
return this.subgroups$.pipe(
|
||||
map((currentGroups: Group[]) => {
|
||||
return createSuccessfulRemoteDataObject(buildPaginatedList<Group>(new PageInfo(), currentGroups));
|
||||
})
|
||||
);
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList<Group>(new PageInfo(), groupsDataServiceStub.getSubgroups()));
|
||||
},
|
||||
getGroupEditPageRouterLink(group: Group): string {
|
||||
return '/access-control/groups/' + group.id;
|
||||
},
|
||||
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
// This method is used to get all groups which are NOT currently a subgroup member
|
||||
searchNonMemberGroups(query: string, group: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
if (query === '') {
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), allGroups));
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), groupNonMembers));
|
||||
}
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));
|
||||
},
|
||||
addSubGroupToGroup(parentGroup, subgroup: Group): Observable<RestResponse> {
|
||||
this.subgroups$.next([...this.subgroups$.getValue(), subgroup]);
|
||||
addSubGroupToGroup(parentGroup, subgroupToAdd: Group): Observable<RestResponse> {
|
||||
// Add group to list of subgroups
|
||||
this.subgroups = [...this.subgroups, subgroupToAdd];
|
||||
// Remove group from list of non-members
|
||||
this.groupNonMembers.forEach( (group: Group, index: number) => {
|
||||
if (group.id === subgroupToAdd.id) {
|
||||
this.groupNonMembers.splice(index, 1);
|
||||
}
|
||||
});
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
},
|
||||
clearGroupsRequests() {
|
||||
@@ -85,12 +110,15 @@ describe('SubgroupsListComponent', () => {
|
||||
clearGroupLinkRequests() {
|
||||
// empty
|
||||
},
|
||||
deleteSubGroupFromGroup(parentGroup, subgroup: Group): Observable<RestResponse> {
|
||||
this.subgroups$.next(this.subgroups$.getValue().filter((group: Group) => {
|
||||
if (group.id !== subgroup.id) {
|
||||
return group;
|
||||
deleteSubGroupFromGroup(parentGroup, subgroupToDelete: Group): Observable<RestResponse> {
|
||||
// Remove group from list of subgroups
|
||||
this.subgroups.forEach( (group: Group, index: number) => {
|
||||
if (group.id === subgroupToDelete.id) {
|
||||
this.subgroups.splice(index, 1);
|
||||
}
|
||||
}));
|
||||
});
|
||||
// Add group to list of non-members
|
||||
this.groupNonMembers = [...this.groupNonMembers, subgroupToDelete];
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
}
|
||||
};
|
||||
@@ -99,7 +127,7 @@ describe('SubgroupsListComponent', () => {
|
||||
translateService = getMockTranslateService();
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
TestBed.configureTestingModule({
|
||||
return TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
@@ -137,30 +165,38 @@ describe('SubgroupsListComponent', () => {
|
||||
expect(comp).toBeDefined();
|
||||
}));
|
||||
|
||||
it('should show list of subgroups of current active group', () => {
|
||||
const groupIdsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tr td:first-child'));
|
||||
expect(groupIdsFound.length).toEqual(1);
|
||||
activeGroup.subgroups.map((group: Group) => {
|
||||
expect(groupIdsFound.find((foundEl) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === group.uuid);
|
||||
})).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if first group delete button is pressed', () => {
|
||||
let groupsFound: DebugElement[];
|
||||
beforeEach(fakeAsync(() => {
|
||||
const addButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton'));
|
||||
addButton.triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
describe('current subgroup list', () => {
|
||||
it('should show list of subgroups of current active group', () => {
|
||||
const groupIdsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tr td:first-child'));
|
||||
expect(groupIdsFound.length).toEqual(1);
|
||||
subgroups.map((group: Group) => {
|
||||
expect(groupIdsFound.find((foundEl) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === group.uuid);
|
||||
})).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show a delete button next to each subgroup', () => {
|
||||
const subgroupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr'));
|
||||
subgroupsFound.map((foundGroupRowElement: DebugElement) => {
|
||||
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
|
||||
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||
expect(addButton).toBeNull();
|
||||
expect(deleteButton).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if first group delete button is pressed', () => {
|
||||
let groupsFound: DebugElement[];
|
||||
beforeEach(() => {
|
||||
const deleteButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton'));
|
||||
deleteButton.nativeElement.click();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('then no subgroup remains as a member of the active group', () => {
|
||||
groupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr'));
|
||||
expect(groupsFound.length).toEqual(0);
|
||||
});
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('one less subgroup in list from 1 to 0 (of 2 total groups)', () => {
|
||||
groupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr'));
|
||||
expect(groupsFound.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,54 +205,38 @@ describe('SubgroupsListComponent', () => {
|
||||
let groupsFound: DebugElement[];
|
||||
beforeEach(fakeAsync(() => {
|
||||
component.search({ query: '' });
|
||||
fixture.detectChanges();
|
||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||
}));
|
||||
|
||||
it('should display all groups', () => {
|
||||
fixture.detectChanges();
|
||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||
expect(groupsFound.length).toEqual(2);
|
||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||
it('should display only non-member groups (i.e. groups that are not a subgroup)', () => {
|
||||
const groupIdsFound: DebugElement[] = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child'));
|
||||
allGroups.map((group: Group) => {
|
||||
expect(groupIdsFound.length).toEqual(1);
|
||||
groupNonMembers.map((group: Group) => {
|
||||
expect(groupIdsFound.find((foundEl: DebugElement) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === group.uuid);
|
||||
})).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if group is already a subgroup', () => {
|
||||
it('should have delete button, else it should have add button', () => {
|
||||
it('should display an add button next to non-member groups, not a delete button', () => {
|
||||
groupsFound.map((foundGroupRowElement: DebugElement) => {
|
||||
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
|
||||
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||
expect(addButton).not.toBeNull();
|
||||
expect(deleteButton).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if first add button is pressed', () => {
|
||||
beforeEach(() => {
|
||||
const addButton: DebugElement = fixture.debugElement.query(By.css('#groupsSearch tbody .fa-plus'));
|
||||
addButton.nativeElement.click();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it('then all (two) Groups are subgroups of the active group. No non-members left', () => {
|
||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||
const getSubgroups = groupsDataServiceStub.getSubgroups().subgroups;
|
||||
if (getSubgroups !== undefined && getSubgroups.length > 0) {
|
||||
groupsFound.map((foundGroupRowElement: DebugElement) => {
|
||||
const groupId: DebugElement = foundGroupRowElement.query(By.css('td:first-child'));
|
||||
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
|
||||
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||
expect(addButton).toBeNull();
|
||||
if (activeGroup.id === groupId.nativeElement.textContent) {
|
||||
expect(deleteButton).toBeNull();
|
||||
} else {
|
||||
expect(deleteButton).not.toBeNull();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const subgroupIds: string[] = activeGroup.subgroups.map((group: Group) => group.id);
|
||||
groupsFound.map((foundGroupRowElement: DebugElement) => {
|
||||
const groupId: DebugElement = foundGroupRowElement.query(By.css('td:first-child'));
|
||||
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
|
||||
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||
if (subgroupIds.includes(groupId.nativeElement.textContent)) {
|
||||
expect(addButton).toBeNull();
|
||||
expect(deleteButton).not.toBeNull();
|
||||
} else {
|
||||
expect(deleteButton).toBeNull();
|
||||
expect(addButton).not.toBeNull();
|
||||
}
|
||||
});
|
||||
}
|
||||
expect(groupsFound.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -2,16 +2,15 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { UntypedFormBuilder } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { map, mergeMap, switchMap, take } from 'rxjs/operators';
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||
import { Group } from '../../../../core/eperson/models/group.model';
|
||||
import {
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteData,
|
||||
getRemoteDataPayload
|
||||
getAllCompletedRemoteData,
|
||||
getFirstCompletedRemoteData
|
||||
} from '../../../../core/shared/operators';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||
@@ -103,6 +102,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
if (activeGroup != null) {
|
||||
this.groupBeingEdited = activeGroup;
|
||||
this.retrieveSubGroups();
|
||||
this.search({query: ''});
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -131,47 +131,6 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the given group is a subgroup of the group currently being edited
|
||||
* @param possibleSubgroup Group that is a possible subgroup (being tested) of the group currently being edited
|
||||
*/
|
||||
isSubgroupOfGroup(possibleSubgroup: Group): Observable<boolean> {
|
||||
return this.groupDataService.getActiveGroup().pipe(take(1),
|
||||
mergeMap((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
if (activeGroup.uuid === possibleSubgroup.uuid) {
|
||||
return observableOf(false);
|
||||
} else {
|
||||
return this.groupDataService.findListByHref(activeGroup._links.subgroups.href, {
|
||||
currentPage: 1,
|
||||
elementsPerPage: 9999
|
||||
})
|
||||
.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((listTotalGroups: PaginatedList<Group>) => listTotalGroups.page.filter((groupInList: Group) => groupInList.id === possibleSubgroup.id)),
|
||||
map((groups: Group[]) => groups.length > 0));
|
||||
}
|
||||
} else {
|
||||
return observableOf(false);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the given group is the current group being edited
|
||||
* @param group Group that is possibly the current group being edited
|
||||
*/
|
||||
isActiveGroup(group: Group): Observable<boolean> {
|
||||
return this.groupDataService.getActiveGroup().pipe(take(1),
|
||||
mergeMap((activeGroup: Group) => {
|
||||
if (activeGroup != null && activeGroup.uuid === group.uuid) {
|
||||
return observableOf(true);
|
||||
}
|
||||
return observableOf(false);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes given subgroup from the group currently being edited
|
||||
* @param subgroup Group we want to delete from the subgroups of the group currently being edited
|
||||
@@ -181,6 +140,11 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
if (activeGroup != null) {
|
||||
const response = this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup);
|
||||
this.showNotifications('deleteSubgroup', response, this.dsoNameService.getName(subgroup), activeGroup);
|
||||
// Reload search results (if there is an active query).
|
||||
// This will potentially add this deleted subgroup into the list of search results.
|
||||
if (this.currentSearchQuery != null) {
|
||||
this.search({query: this.currentSearchQuery});
|
||||
}
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||
}
|
||||
@@ -197,6 +161,11 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
if (activeGroup.uuid !== subgroup.uuid) {
|
||||
const response = this.groupDataService.addSubGroupToGroup(activeGroup, subgroup);
|
||||
this.showNotifications('addSubgroup', response, this.dsoNameService.getName(subgroup), activeGroup);
|
||||
// Reload search results (if there is an active query).
|
||||
// This will potentially remove this added subgroup from search results.
|
||||
if (this.currentSearchQuery != null) {
|
||||
this.search({query: this.currentSearchQuery});
|
||||
}
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.subgroupToAddIsActiveGroup'));
|
||||
}
|
||||
@@ -207,28 +176,38 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Search in the groups (searches by group name and by uuid exact match)
|
||||
* Search all non-member groups (searches by group name and by uuid exact match). Used to search for
|
||||
* groups that could be added to current group as a subgroup.
|
||||
* @param data Contains query param
|
||||
*/
|
||||
search(data: any) {
|
||||
const query: string = data.query;
|
||||
if (query != null && this.currentSearchQuery !== query) {
|
||||
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited));
|
||||
this.currentSearchQuery = query;
|
||||
this.configSearch.currentPage = 1;
|
||||
}
|
||||
this.searchDone = true;
|
||||
|
||||
this.unsubFrom(SubKey.SearchResults);
|
||||
this.subs.set(SubKey.SearchResults, this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe(
|
||||
switchMap((config) => this.groupDataService.searchGroups(this.currentSearchQuery, {
|
||||
currentPage: config.currentPage,
|
||||
elementsPerPage: config.pageSize
|
||||
}, true, true, followLink('object')
|
||||
))
|
||||
).subscribe((rd: RemoteData<PaginatedList<Group>>) => {
|
||||
this.searchResults$.next(rd);
|
||||
}));
|
||||
this.subs.set(SubKey.SearchResults,
|
||||
this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe(
|
||||
switchMap((paginationOptions) => {
|
||||
const query: string = data.query;
|
||||
if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) {
|
||||
this.currentSearchQuery = query;
|
||||
this.paginationService.resetPage(this.configSearch.id);
|
||||
}
|
||||
this.searchDone = true;
|
||||
|
||||
return this.groupDataService.searchNonMemberGroups(this.currentSearchQuery, this.groupBeingEdited.id, {
|
||||
currentPage: paginationOptions.currentPage,
|
||||
elementsPerPage: paginationOptions.pageSize
|
||||
}, false, true, followLink('object'));
|
||||
}),
|
||||
getAllCompletedRemoteData(),
|
||||
map((rd: RemoteData<any>) => {
|
||||
if (rd.hasFailed) {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', { cause: rd.errorMessage }));
|
||||
} else {
|
||||
return rd;
|
||||
}
|
||||
}))
|
||||
.subscribe((rd: RemoteData<PaginatedList<Group>>) => {
|
||||
this.searchResults$.next(rd);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -2,17 +2,17 @@
|
||||
<div class="groups-registry row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between border-bottom mb-3">
|
||||
<h2 id="header" class="pb-2">{{messagePrefix + 'head' | translate}}</h2>
|
||||
<h1 id="header" class="pb-2">{{messagePrefix + 'head' | translate}}</h1>
|
||||
<div>
|
||||
<button class="mr-auto btn btn-success"
|
||||
[routerLink]="['newGroup']">
|
||||
[routerLink]="'create'">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span class="d-none d-sm-inline ml-1">{{messagePrefix + 'button.add' | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 id="search" class="border-bottom pb-2">{{messagePrefix + 'search.head' | translate}}</h3>
|
||||
<h2 id="search" class="border-bottom pb-2">{{messagePrefix + 'search.head' | translate}}</h2>
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||
<div class="flex-grow-1 mr-3">
|
||||
<div class="form-group input-group">
|
||||
|
@@ -216,18 +216,28 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* Get the members (epersons embedded value of a group)
|
||||
* NOTE: At this time we only grab the *first* member in order to receive the `totalElements` value
|
||||
* needed for our HTML template.
|
||||
* @param group
|
||||
*/
|
||||
getMembers(group: Group): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
return this.ePersonDataService.findListByHref(group._links.epersons.href).pipe(getFirstSucceededRemoteData());
|
||||
return this.ePersonDataService.findListByHref(group._links.epersons.href, {
|
||||
currentPage: 1,
|
||||
elementsPerPage: 1,
|
||||
}).pipe(getFirstSucceededRemoteData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subgroups (groups embedded value of a group)
|
||||
* NOTE: At this time we only grab the *first* subgroup in order to receive the `totalElements` value
|
||||
* needed for our HTML template.
|
||||
* @param group
|
||||
*/
|
||||
getSubgroups(group: Group): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
return this.groupService.findListByHref(group._links.subgroups.href).pipe(getFirstSucceededRemoteData());
|
||||
return this.groupService.findListByHref(group._links.subgroups.href, {
|
||||
currentPage: 1,
|
||||
elementsPerPage: 1,
|
||||
}).pipe(getFirstSucceededRemoteData());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div class="container">
|
||||
<h2>{{'admin.curation-tasks.header' |translate }}</h2>
|
||||
<h1>{{'admin.curation-tasks.header' |translate }}</h1>
|
||||
<ds-curation-form></ds-curation-form>
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="container">
|
||||
<h2 id="header">{{'admin.metadata-import.page.header' | translate}}</h2>
|
||||
<h1 id="header">{{'admin.metadata-import.page.header' | translate}}</h1>
|
||||
<p>{{'admin.metadata-import.page.help' | translate}}</p>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
|
@@ -1,33 +1,45 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {I18nBreadcrumbResolver} from 'src/app/core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import {LdnServicesOverviewComponent} from './ldn-services-directory/ldn-services-directory.component';
|
||||
import {LdnServiceNewComponent} from './ldn-service-new/ldn-service-new.component';
|
||||
import {LdnServiceFormEditComponent} from './ldn-service-form-edit/ldn-service-form-edit.component';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component';
|
||||
import { NavigationBreadcrumbResolver } from '../../core/breadcrumbs/navigation-breadcrumb.resolver';
|
||||
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component';
|
||||
|
||||
|
||||
const moduleRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
component: LdnServicesOverviewComponent,
|
||||
resolve: {breadcrumb: I18nBreadcrumbResolver},
|
||||
data: {title: 'ldn-registered-services.title', breadcrumbKey: 'ldn-registered-services.new'},
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
resolve: {breadcrumb: NavigationBreadcrumbResolver},
|
||||
component: LdnServiceFormComponent,
|
||||
data: {title: 'ldn-register-new-service.title', breadcrumbKey: 'ldn-register-new-service'}
|
||||
},
|
||||
{
|
||||
path: 'edit/:serviceId',
|
||||
resolve: {breadcrumb: NavigationBreadcrumbResolver},
|
||||
component: LdnServiceFormComponent,
|
||||
data: {title: 'ldn-edit-service.title', breadcrumbKey: 'ldn-edit-service'}
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
component: LdnServicesOverviewComponent,
|
||||
resolve: {breadcrumb: I18nBreadcrumbResolver},
|
||||
data: {title: 'ldn-registered-services.title', breadcrumbKey: 'ldn-registered-services.new'},
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
resolve: {breadcrumb: I18nBreadcrumbResolver},
|
||||
component: LdnServiceNewComponent,
|
||||
data: {title: 'ldn-register-new-service.title', breadcrumbKey: 'ldn-register-new-service'}
|
||||
},
|
||||
{
|
||||
path: 'edit/:serviceId',
|
||||
resolve: {breadcrumb: I18nBreadcrumbResolver},
|
||||
component: LdnServiceFormEditComponent,
|
||||
data: {title: 'ldn-edit-service.title', breadcrumbKey: 'ldn-edit-service'}
|
||||
},
|
||||
]),
|
||||
RouterModule.forChild(moduleRoutes.map(route => {
|
||||
return {...route, data: {
|
||||
...route.data,
|
||||
relatedRoutes: moduleRoutes.filter(relatedRoute => relatedRoute.path !== route.path)
|
||||
.map((relatedRoute) => {
|
||||
return {path: relatedRoute.path, data: relatedRoute.data};
|
||||
})
|
||||
}};
|
||||
}))
|
||||
]
|
||||
})
|
||||
export class AdminLdnServicesRoutingModule {
|
||||
|
@@ -1,13 +1,11 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {AdminLdnServicesRoutingModule} from './admin-ldn-services-routing.module';
|
||||
import {LdnServicesOverviewComponent} from './ldn-services-directory/ldn-services-directory.component';
|
||||
import {SharedModule} from '../../shared/shared.module';
|
||||
import {LdnServiceNewComponent} from './ldn-service-new/ldn-service-new.component';
|
||||
import {LdnServiceFormComponent} from './ldn-service-form/ldn-service-form.component';
|
||||
import {LdnServiceFormEditComponent} from './ldn-service-form-edit/ldn-service-form-edit.component';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {LdnItemfiltersService} from './ldn-services-data/ldn-itemfilters-data.service';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { AdminLdnServicesRoutingModule } from './admin-ldn-services-routing.module';
|
||||
import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { LdnItemfiltersService } from './ldn-services-data/ldn-itemfilters-data.service';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@@ -19,9 +17,7 @@ import {LdnItemfiltersService} from './ldn-services-data/ldn-itemfilters-data.se
|
||||
],
|
||||
declarations: [
|
||||
LdnServicesOverviewComponent,
|
||||
LdnServiceNewComponent,
|
||||
LdnServiceFormComponent,
|
||||
LdnServiceFormEditComponent,
|
||||
],
|
||||
providers: [LdnItemfiltersService]
|
||||
})
|
||||
|
@@ -1,415 +0,0 @@
|
||||
<div class="container">
|
||||
<form (ngSubmit)="onSubmit()" [formGroup]="formModel">
|
||||
<div class="d-flex">
|
||||
<h2 class="flex-grow-1">{{ 'ldn-edit-registered-service.title' | translate }}</h2>
|
||||
</div>
|
||||
<!-- In the toggle section -->
|
||||
<div class="toggle-switch-container">
|
||||
<label class="status-label font-weight-bold" for="enabled">{{ 'ldn-service-status' | translate }}</label>
|
||||
<div>
|
||||
<input formControlName="enabled" hidden id="enabled" name="enabled" type="checkbox">
|
||||
<div (click)="toggleEnabled()" [class.checked]="formModel.get('enabled').value" class="toggle-switch">
|
||||
<div class="slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- In the Name section -->
|
||||
<div class="mb-5">
|
||||
<label for="name" class="font-weight-bold">{{ 'ldn-new-service.form.label.name' | translate }}</label>
|
||||
<input [class.invalid-field]="formModel.get('name').invalid && formModel.get('name').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.name' | translate" class="form-control"
|
||||
formControlName="name"
|
||||
id="name"
|
||||
name="name"
|
||||
type="text">
|
||||
<div *ngIf="formModel.get('name').invalid && formModel.get('name').touched" class="error-text">
|
||||
{{ 'ldn-new-service.form.error.name' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- In the description section -->
|
||||
<div class="mb-5 mt-5 d-flex flex-column">
|
||||
<label for="description" class="font-weight-bold">{{ 'ldn-new-service.form.label.description' | translate }}</label>
|
||||
<textarea [placeholder]="'ldn-new-service.form.placeholder.description' | translate"
|
||||
class="form-control" formControlName="description" id="description" name="description"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- In the url section -->
|
||||
<div class="mb-5 mt-5">
|
||||
<label for="url" class="font-weight-bold">{{ 'ldn-new-service.form.label.url' | translate }}</label>
|
||||
<input [class.invalid-field]="formModel.get('url').invalid && formModel.get('url').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.url' | translate" class="form-control"
|
||||
formControlName="url"
|
||||
id="url"
|
||||
name="url"
|
||||
type="text">
|
||||
<div *ngIf="formModel.get('url').invalid && formModel.get('url').touched" class="error-text">
|
||||
{{ 'ldn-new-service.form.error.url' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- In the ldnUrl section -->
|
||||
<div class="mb-5 mt-5">
|
||||
<label for="ldnUrl" class="font-weight-bold">{{ 'ldn-new-service.form.label.ldnUrl' | translate }}</label>
|
||||
<input [class.invalid-field]="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.ldnUrl' | translate" class="form-control"
|
||||
formControlName="ldnUrl"
|
||||
id="ldnUrl"
|
||||
name="ldnUrl"
|
||||
type="text">
|
||||
<div *ngIf="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched" class="error-text">
|
||||
{{ 'ldn-new-service.form.error.ldnurl' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- In the score section -->
|
||||
<div class="mb-2">
|
||||
<label for="score" class="font-weight-bold">{{ 'ldn-new-service.form.label.score' | translate }}</label>
|
||||
<input [class.invalid-field]="formModel.get('score').invalid && formModel.get('score').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.score' | translate" formControlName="score"
|
||||
id="score"
|
||||
name="score"
|
||||
min="0"
|
||||
max="1"
|
||||
step=".01"
|
||||
class="form-control"
|
||||
type="number">
|
||||
<div *ngIf="formModel.get('score').invalid && formModel.get('score').touched" class="error-text">
|
||||
{{ 'ldn-new-service.form.error.score' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- In the Inbound Patterns Labels section -->
|
||||
<div class="row mb-1 mt-5">
|
||||
<div class="col">
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.inboundPattern' | translate }} </label>
|
||||
</div>
|
||||
<ng-container *ngIf="!!(formModel.get('notifyServiceInboundPatterns')['controls'][0]?.value?.pattern)">
|
||||
<div class="col">
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.automatic' | translate }}</label>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="col-sm-2">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- In the Inbound Patterns section -->
|
||||
<div *ngFor="let patternGroup of formModel.get('notifyServiceInboundPatterns')['controls']; let i = index"
|
||||
[class.marked-for-deletion]="markedForDeletionInboundPattern.includes(i)"
|
||||
formGroupName="notifyServiceInboundPatterns">
|
||||
|
||||
<ng-container [formGroupName]="i">
|
||||
|
||||
|
||||
<div class="row mb-1">
|
||||
<div class="col">
|
||||
<div #inboundPatternDropdown="ngbDropdown" class="w-80" display="dynamic"
|
||||
id="additionalInboundPattern{{i}}"
|
||||
ngbDropdown placement="bottom-start">
|
||||
<div class="position-relative right-addon" role="combobox">
|
||||
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
|
||||
ngbDropdownToggle></i>
|
||||
<input
|
||||
(click)="inboundPatternDropdown.open();"
|
||||
[readonly]="true"
|
||||
class="form-control w-80 scrollable-dropdown-input"
|
||||
formControlName="patternLabel"
|
||||
id="inboundPatternDropdownButton"
|
||||
ngbDropdownAnchor
|
||||
type="text"
|
||||
/>
|
||||
<div aria-labelledby="inboundPatternDropdownButton"
|
||||
class="dropdown-menu scrollable-dropdown-menu w-100 "
|
||||
ngbDropdownMenu>
|
||||
<div class="scrollable-menu" role="listbox">
|
||||
<button (click)="selectInboundPattern(pattern, i); $event.stopPropagation()"
|
||||
*ngFor="let pattern of inboundPatterns; let internalIndex = index"
|
||||
[title]="'ldn-service.form.pattern.' + pattern + '.description' | translate"
|
||||
class="dropdown-item collection-item text-truncate w-100"
|
||||
ngbDropdownItem
|
||||
type="button">
|
||||
<div>{{ 'ldn-service.form.pattern.' + pattern + '.label' | translate }}</div>
|
||||
<div
|
||||
class="small-text">{{ 'ldn-service.form.pattern.' + pattern + '.description' | translate }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<ng-container
|
||||
*ngIf="!!(formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern)">
|
||||
<div #inboundItemfilterDropdown="ngbDropdown" class="w-100" id="constraint{{i}}" ngbDropdown
|
||||
placement="bottom-start">
|
||||
<div class="position-relative right-addon" role="combobox">
|
||||
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
|
||||
ngbDropdownToggle></i>
|
||||
<input
|
||||
(click)="inboundItemfilterDropdown.open();"
|
||||
[readonly]="true"
|
||||
[value]="selectedInboundItemfilters"
|
||||
class="form-control w-100 scrollable-dropdown-input"
|
||||
formControlName="constraint"
|
||||
id="inboundItemfilterDropdown"
|
||||
ngbDropdownAnchor
|
||||
type="text"
|
||||
/>
|
||||
<div aria-labelledby="inboundItemfilterDropdownButton"
|
||||
class="dropdown-menu scrollable-dropdown-menu w-100 "
|
||||
ngbDropdownMenu>
|
||||
<div class="scrollable-menu" role="listbox">
|
||||
<button (click)="selectInboundItemFilter('', i); $event.stopPropagation()"
|
||||
class="dropdown-item collection-item text-truncate w-100" ngbDropdownItem type="button">
|
||||
<span> {{'ldn-service.control-constaint-select-none' | translate}} </span>
|
||||
</button>
|
||||
<button (click)="selectInboundItemFilter(constraint.id, i); $event.stopPropagation()"
|
||||
*ngFor="let constraint of (itemfiltersRD$ | async)?.payload?.page; let internalIndex = index"
|
||||
class="dropdown-item collection-item text-truncate w-100"
|
||||
ngbDropdownItem
|
||||
type="button">
|
||||
<div>{{ constraint.id }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div
|
||||
[style.visibility]="formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern ? 'visible' : 'hidden'"
|
||||
class="col-sm-1">
|
||||
<input formControlName="automatic" hidden id="automatic{{i}}" name="automatic{{i}}"
|
||||
type="checkbox">
|
||||
<div (click)="toggleAutomatic(i)"
|
||||
[class.checked]="formModel.get('notifyServiceInboundPatterns.' + i + '.automatic').value"
|
||||
class="toggle-switch">
|
||||
<div class="slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-sm-2">
|
||||
<div class="btn-group">
|
||||
<button (click)="markForInboundPatternDeletion(i)" class="btn btn-outline-dark trash-button"
|
||||
type="button">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
|
||||
|
||||
<button (click)="unmarkForInboundPatternDeletion(i)"
|
||||
*ngIf="markedForDeletionInboundPattern.includes(i)"
|
||||
class="btn btn-warning "
|
||||
type="button">
|
||||
<i class="fas fa-undo"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
|
||||
<span (click)="addInboundPattern()"
|
||||
class="add-pattern-link mb-2">{{ 'ldn-new-service.form.label.addPattern' | translate }}</span>
|
||||
|
||||
<!-- In the Outbound Patterns Labels section -->
|
||||
<div class="row mb-1 mt-5">
|
||||
<div class="col">
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.outboundPattern' | translate }} </label>
|
||||
</div>
|
||||
<ng-container *ngIf="!!(formModel.get('notifyServiceOutboundPatterns')['controls'][0]?.value?.pattern)">
|
||||
<div class="col">
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="col-sm-1 ">
|
||||
<label class="label-box-2" style="visibility: hidden;">
|
||||
{{ 'ldn-new-service.form.label.automatic' | translate }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-sm-2 ">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- In the Outbound Patterns section -->
|
||||
<div *ngFor="let patternGroup of formModel.get('notifyServiceOutboundPatterns')['controls']; let i = index"
|
||||
[class.marked-for-deletion]="markedForDeletionOutboundPattern.includes(i)"
|
||||
formGroupName="notifyServiceOutboundPatterns">
|
||||
|
||||
<ng-container [formGroupName]="i">
|
||||
|
||||
<div class="row mb-1">
|
||||
<div class="col">
|
||||
<div #outboundPatternDropdown="ngbDropdown" class="w-100" id="additionalOutboundPattern{{i}}"
|
||||
ngbDropdown
|
||||
placement="bottom-start">
|
||||
<div class="position-relative right-addon" role="combobox">
|
||||
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
|
||||
ngbDropdownToggle></i>
|
||||
<input
|
||||
(click)="outboundPatternDropdown.open();"
|
||||
[readonly]="true"
|
||||
[value]="selectedOutboundPatterns"
|
||||
class="form-control w-100 scrollable-dropdown-input"
|
||||
formControlName="patternLabel"
|
||||
id="outboundPatternDropdownButton"
|
||||
ngbDropdownAnchor
|
||||
type="text"
|
||||
/>
|
||||
<div aria-labelledby="outboundPatternDropdownButton"
|
||||
class="dropdown-menu scrollable-dropdown-menu w-100 "
|
||||
ngbDropdownMenu>
|
||||
<div class="scrollable-menu" role="listbox">
|
||||
<button (click)="selectOutboundPattern(pattern, i); $event.stopPropagation()"
|
||||
*ngFor="let pattern of outboundPatterns; let internalIndex = index"
|
||||
[title]="'ldn-service.form.pattern.' + pattern + '.description' | translate"
|
||||
class="dropdown-item collection-item text-truncate w-100"
|
||||
ngbDropdownItem
|
||||
type="button">
|
||||
<div>{{ 'ldn-service.form.pattern.' + pattern + '.label' | translate }}</div>
|
||||
<div
|
||||
class="small-text">{{ 'ldn-service.form.pattern.' + pattern + '.description' | translate }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<ng-container
|
||||
*ngIf="!!(formModel.get('notifyServiceOutboundPatterns')['controls'][i].value.pattern)">
|
||||
<div #outboundItemfilterDropdown="ngbDropdown" class="w-100" id="constraint{{i}}"
|
||||
ngbDropdown
|
||||
placement="bottom-start">
|
||||
<div class="position-relative right-addon" role="combobox">
|
||||
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
|
||||
ngbDropdownToggle></i>
|
||||
<input
|
||||
(click)="outboundItemfilterDropdown.open();"
|
||||
[readonly]="true"
|
||||
[value]="selectedOutboundItemfilters"
|
||||
class="form-control w-100 scrollable-dropdown-input"
|
||||
formControlName="constraint"
|
||||
id="outboundItemfilterDropdown"
|
||||
ngbDropdownAnchor
|
||||
type="text"
|
||||
/>
|
||||
<div aria-labelledby="outboundItemfilterDropdownButton"
|
||||
class="dropdown-menu scrollable-dropdown-menu w-100 "
|
||||
ngbDropdownMenu>
|
||||
<div class="scrollable-menu" role="listbox">
|
||||
<button (click)="selectOutboundItemFilter('', i); $event.stopPropagation()"
|
||||
class="dropdown-item collection-item text-truncate w-100" ngbDropdownItem type="button">
|
||||
<span> {{'ldn-service.control-constaint-select-none' | translate}} </span>
|
||||
</button>
|
||||
<button (click)="selectOutboundItemFilter(constraint.id, i); $event.stopPropagation()"
|
||||
*ngFor="let constraint of (itemfiltersRD$ | async)?.payload?.page; let internalIndex = index"
|
||||
class="dropdown-item collection-item text-truncate w-100"
|
||||
ngbDropdownItem
|
||||
type="button">
|
||||
<div>{{ constraint.id }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div [style.visibility]="'hidden'" class="col-sm-1">
|
||||
<input hidden id="automatic{{i}}" name="automatic{{i}}" type="checkbox">
|
||||
<div
|
||||
class="toggle-switch">
|
||||
<div class="slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2">
|
||||
<div class="btn-group">
|
||||
<button (click)="markForOutboundPatternDeletion(i)"
|
||||
class="btn btn-outline-dark trash-button" type="button">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<button (click)="unmarkForOutboundPatternDeletion(i)"
|
||||
*ngIf="markedForDeletionOutboundPattern.includes(i)"
|
||||
class="btn btn-warning "
|
||||
type="button">
|
||||
<i class="fas fa-undo"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div
|
||||
(click)="addOutboundPattern()"
|
||||
class="add-pattern-link mb-4">{{ 'ldn-new-service.form.label.addPattern' | translate }}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="submission-form-footer my-1 position-sticky d-flex justify-content-between" role="group">
|
||||
<button (click)="openResetFormModal(resetFormModal)" class="btn btn-danger" type="button">
|
||||
<span><i class="fas fa-trash"></i> {{ 'submission.general.discard.submit' | translate }}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
<span><i class="fas fa-save"></i> {{ 'ldn-new-service.form.label.submit' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<ng-template #confirmModal>
|
||||
<div class="modal-header">
|
||||
<h4>{{'service.overview.edit.modal' | translate }}</h4>
|
||||
<button (click)="closeModal()" aria-label="Close"
|
||||
class="close" type="button">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div>
|
||||
{{ 'service.overview.edit.body' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button (click)="closeModal()" class="btn btn-danger mr-2"
|
||||
id="delete-confirm">{{ 'service.detail.return' | translate }}
|
||||
</button>
|
||||
<button (click)="patchService()"
|
||||
class="btn btn-primary">{{ 'service.detail.update' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #resetFormModal>
|
||||
<div class="modal-header">
|
||||
<h4 class="text-danger">{{'service.overview.reset-form.modal' | translate }}</h4>
|
||||
<button (click)="closeModal()" aria-label="Close"
|
||||
class="close" type="button">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<span>
|
||||
{{ 'service.overview.reset-form.body' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button (click)="closeModal()"
|
||||
class="btn btn-primary"
|
||||
id="reset-confirm">{{ 'service.overview.reset-form.reset-return' | translate }}
|
||||
</button>
|
||||
<button (click)="resetFormAndLeave()" class="mr-2 btn btn-danger"
|
||||
id="reset-delete">{{ 'service.overview.reset-form.reset-confirm' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@@ -1,141 +0,0 @@
|
||||
@import '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss';
|
||||
@import '../../../shared/form/form.component.scss';
|
||||
|
||||
form {
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
select {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
option:not(:first-child) {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.trash-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 200px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.add-pattern-link {
|
||||
color: #0048ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.remove-pattern-link {
|
||||
color: #e34949;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.status-checkbox {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
|
||||
.invalid-field {
|
||||
border: 1px solid red;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: red;
|
||||
font-size: 0.8em;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.8;
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
background-color: #ccc;
|
||||
border-radius: 15px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.toggle-switch.checked {
|
||||
background-color: #24cc9a;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
|
||||
.toggle-switch .slider {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.toggle-switch.checked .slider {
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
.toggle-switch-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.small-text {
|
||||
font-size: 0.7em;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
cursor: pointer;
|
||||
margin-top: 3px;
|
||||
margin-right: 3px
|
||||
}
|
||||
|
||||
.label-box {
|
||||
margin-left: 11px;
|
||||
}
|
||||
|
||||
.label-box-2 {
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
.label-box-3 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.submission-form-footer {
|
||||
border-radius: var(--bs-card-border-radius);
|
||||
bottom: 0;
|
||||
background-color: var(--bs-gray-400);
|
||||
padding: calc(var(--bs-spacer) / 2);
|
||||
z-index: var(--ds-submission-footer-z-index);
|
||||
}
|
||||
|
||||
.marked-for-deletion {
|
||||
background-color: lighten($red, 30%);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@@ -1,80 +0,0 @@
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {NgbDropdownModule, NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {LdnServiceFormEditComponent} from './ldn-service-form-edit.component';
|
||||
import {ChangeDetectorRef, EventEmitter} from '@angular/core';
|
||||
import {FormBuilder, ReactiveFormsModule} from '@angular/forms';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {TranslateModule, TranslateService} from '@ngx-translate/core';
|
||||
import {PaginationService} from 'ngx-pagination';
|
||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||
import {LdnItemfiltersService} from '../ldn-services-data/ldn-itemfilters-data.service';
|
||||
import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service';
|
||||
import {RouterStub} from '../../../shared/testing/router.stub';
|
||||
import {MockActivatedRoute} from '../../../shared/mocks/active-router.mock';
|
||||
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service.stub';
|
||||
import {of} from 'rxjs';
|
||||
import {RouteService} from '../../../core/services/route.service';
|
||||
import {provideMockStore} from '@ngrx/store/testing';
|
||||
|
||||
describe('LdnServiceFormEditComponent', () => {
|
||||
let component: LdnServiceFormEditComponent;
|
||||
let fixture: ComponentFixture<LdnServiceFormEditComponent>;
|
||||
|
||||
let ldnServicesService: any;
|
||||
let ldnItemfiltersService: any;
|
||||
let cdRefStub: any;
|
||||
let modalService: any;
|
||||
|
||||
const translateServiceStub = {
|
||||
get: () => of('translated-text'),
|
||||
instant: () => 'translated-text',
|
||||
onLangChange: new EventEmitter(),
|
||||
onTranslationChange: new EventEmitter(),
|
||||
onDefaultLangChange: new EventEmitter()
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
ldnServicesService = {
|
||||
update: () => ({}),
|
||||
};
|
||||
ldnItemfiltersService = {
|
||||
findAll: () => of(['item1', 'item2']),
|
||||
};
|
||||
cdRefStub = Object.assign({
|
||||
detectChanges: () => fixture.detectChanges()
|
||||
});
|
||||
modalService = {
|
||||
open: () => {/*comment*/
|
||||
}
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ReactiveFormsModule, TranslateModule.forRoot(), NgbDropdownModule],
|
||||
declarations: [LdnServiceFormEditComponent],
|
||||
providers: [
|
||||
{provide: LdnServicesService, useValue: ldnServicesService},
|
||||
{provide: LdnItemfiltersService, useValue: ldnItemfiltersService},
|
||||
{provide: Router, useValue: new RouterStub()},
|
||||
{provide: ActivatedRoute, useValue: new MockActivatedRoute()},
|
||||
{provide: ChangeDetectorRef, useValue: cdRefStub},
|
||||
{provide: NgbModal, useValue: modalService},
|
||||
{provide: NotificationsService, useValue: NotificationsServiceStub},
|
||||
{provide: TranslateService, useValue: translateServiceStub},
|
||||
{provide: PaginationService, useValue: {}},
|
||||
FormBuilder,
|
||||
RouteService,
|
||||
provideMockStore({}),
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LdnServiceFormEditComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -1,597 +0,0 @@
|
||||
import {ChangeDetectorRef, Component, Input, OnInit, TemplateRef, ViewChild} from '@angular/core';
|
||||
import {FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||
import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service';
|
||||
import {notifyPatterns} from '../ldn-services-patterns/ldn-service-coar-patterns';
|
||||
import {animate, state, style, transition, trigger} from '@angular/animations';
|
||||
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {LdnService} from '../ldn-services-model/ldn-services.model';
|
||||
import {RemoteData} from 'src/app/core/data/remote-data';
|
||||
import {Operation} from 'fast-json-patch';
|
||||
import {getFirstCompletedRemoteData} from '../../../core/shared/operators';
|
||||
import {LdnItemfiltersService} from '../ldn-services-data/ldn-itemfilters-data.service';
|
||||
import {Itemfilter} from '../ldn-services-model/ldn-service-itemfilters';
|
||||
import {PaginatedList} from '../../../core/data/paginated-list.model';
|
||||
import {Observable} from 'rxjs';
|
||||
import {PaginationService} from '../../../core/pagination/pagination.service';
|
||||
import {FindListOptions} from '../../../core/data/find-list-options.model';
|
||||
import {PaginationComponentOptions} from '../../../shared/pagination/pagination-component-options.model';
|
||||
import {NotifyServicePattern} from '../ldn-services-model/ldn-service-patterns.model';
|
||||
|
||||
|
||||
/**
|
||||
* Component for editing LDN service through a form that allows to edit the properties of the selected service
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-ldn-service-form-edit',
|
||||
templateUrl: './ldn-service-form-edit.component.html',
|
||||
styleUrls: ['./ldn-service-form-edit.component.scss'],
|
||||
animations: [
|
||||
trigger('toggleAnimation', [
|
||||
state('true', style({})),
|
||||
state('false', style({})),
|
||||
transition('true <=> false', animate('300ms ease-in')),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class LdnServiceFormEditComponent implements OnInit {
|
||||
formModel: FormGroup;
|
||||
@ViewChild('confirmModal', {static: true}) confirmModal: TemplateRef<any>;
|
||||
@ViewChild('resetFormModal', {static: true}) resetFormModal: TemplateRef<any>;
|
||||
|
||||
public inboundPatterns: string[] = notifyPatterns;
|
||||
public outboundPatterns: string[] = notifyPatterns;
|
||||
itemfiltersRD$: Observable<RemoteData<PaginatedList<Itemfilter>>>;
|
||||
config: FindListOptions = Object.assign(new FindListOptions(), {
|
||||
elementsPerPage: 20
|
||||
});
|
||||
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'po',
|
||||
pageSize: 20
|
||||
});
|
||||
@Input() public name: string;
|
||||
@Input() public description: string;
|
||||
@Input() public url: string;
|
||||
@Input() public ldnUrl: string;
|
||||
@Input() public score: number;
|
||||
@Input() public inboundPattern: string;
|
||||
@Input() public outboundPattern: string;
|
||||
@Input() public constraint: string;
|
||||
@Input() public automatic: boolean;
|
||||
@Input() public headerKey: string;
|
||||
markedForDeletionInboundPattern: number[] = [];
|
||||
markedForDeletionOutboundPattern: number[] = [];
|
||||
selectedOutboundPatterns: string[];
|
||||
selectedInboundItemfilters: string[];
|
||||
selectedOutboundItemfilters: string[];
|
||||
selectedInboundPatterns: string[];
|
||||
protected serviceId: string;
|
||||
private deletedInboundPatterns: number[] = [];
|
||||
private deletedOutboundPatterns: number[] = [];
|
||||
private modalRef: any;
|
||||
private service: LdnService;
|
||||
private selectPatternDefaultLabeli18Key = 'ldn-service.form.label.placeholder.default-select';
|
||||
|
||||
constructor(
|
||||
protected ldnServicesService: LdnServicesService,
|
||||
private ldnItemfiltersService: LdnItemfiltersService,
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private cdRef: ChangeDetectorRef,
|
||||
protected modalService: NgbModal,
|
||||
private notificationService: NotificationsService,
|
||||
private translateService: TranslateService,
|
||||
protected paginationService: PaginationService
|
||||
) {
|
||||
|
||||
this.formModel = this.formBuilder.group({
|
||||
id: [''],
|
||||
name: ['', Validators.required],
|
||||
description: ['', Validators.required],
|
||||
url: ['', Validators.required],
|
||||
ldnUrl: ['', Validators.required],
|
||||
score: ['', [Validators.required, Validators.pattern('^0*(\.[0-9]+)?$|^1(\.0+)?$')]], inboundPattern: [''],
|
||||
outboundPattern: [''],
|
||||
constraintPattern: [''],
|
||||
enabled: [''],
|
||||
notifyServiceInboundPatterns: this.formBuilder.array([this.createInboundPatternFormGroup()]),
|
||||
notifyServiceOutboundPatterns: this.formBuilder.array([this.createOutboundPatternFormGroup()]),
|
||||
type: LDN_SERVICE.value,
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.params.subscribe((params) => {
|
||||
this.serviceId = params.serviceId;
|
||||
if (this.serviceId) {
|
||||
this.fetchServiceData(this.serviceId);
|
||||
}
|
||||
});
|
||||
this.setItemfilters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets item filters using LDN item filters service
|
||||
*/
|
||||
setItemfilters() {
|
||||
this.itemfiltersRD$ = this.ldnItemfiltersService.findAll().pipe(
|
||||
getFirstCompletedRemoteData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches LDN service data by ID and updates the form
|
||||
* @param serviceId - The ID of the LDN service
|
||||
*/
|
||||
fetchServiceData(serviceId: string): void {
|
||||
this.ldnServicesService.findById(serviceId).pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe(
|
||||
(data: RemoteData<LdnService>) => {
|
||||
if (data.hasSucceeded) {
|
||||
this.service = data.payload;
|
||||
|
||||
this.formModel.patchValue({
|
||||
id: this.service.id,
|
||||
name: this.service.name,
|
||||
description: this.service.description,
|
||||
url: this.service.url,
|
||||
score: this.service.score, ldnUrl: this.service.ldnUrl,
|
||||
type: this.service.type,
|
||||
enabled: this.service.enabled
|
||||
});
|
||||
this.filterPatternObjectsAndPickLabel('notifyServiceInboundPatterns', false);
|
||||
this.filterPatternObjectsAndPickLabel('notifyServiceOutboundPatterns', true);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters pattern objects, initializes form groups, assigns labels, and adds them to the specified form array so the correct string is shown in the dropdown..
|
||||
* @param formArrayName - The name of the form array to be populated
|
||||
* @param isOutbound - A boolean indicating whether the patterns are outbound (true) or inbound (false)
|
||||
*/
|
||||
filterPatternObjectsAndPickLabel(formArrayName: string, isOutbound: boolean) {
|
||||
const PatternsArray = this.formModel.get(formArrayName) as FormArray;
|
||||
PatternsArray.clear();
|
||||
let servicesToUse;
|
||||
if (isOutbound) {
|
||||
servicesToUse = this.service.notifyServiceOutboundPatterns;
|
||||
} else {
|
||||
servicesToUse = this.service.notifyServiceInboundPatterns;
|
||||
}
|
||||
|
||||
servicesToUse.forEach((patternObj: NotifyServicePattern) => {
|
||||
let patternFormGroup;
|
||||
if (isOutbound) {
|
||||
patternFormGroup = this.initializeOutboundPatternFormGroup();
|
||||
} else {
|
||||
patternFormGroup = this.initializeInboundPatternFormGroup();
|
||||
}
|
||||
const newPatternObjWithLabel = Object.assign(new NotifyServicePattern(), {
|
||||
...patternObj,
|
||||
patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternObj?.pattern + '.label')
|
||||
});
|
||||
patternFormGroup.patchValue(newPatternObjWithLabel);
|
||||
|
||||
PatternsArray.push(patternFormGroup);
|
||||
this.cdRef.detectChanges();
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of patch operations based on form changes
|
||||
* @returns Array of patch operations
|
||||
*/
|
||||
generatePatchOperations(): any[] {
|
||||
const patchOperations: any[] = [];
|
||||
|
||||
this.createReplaceOperation(patchOperations, 'name', '/name');
|
||||
this.createReplaceOperation(patchOperations, 'description', '/description');
|
||||
this.createReplaceOperation(patchOperations, 'ldnUrl', '/ldnurl');
|
||||
this.createReplaceOperation(patchOperations, 'url', '/url');
|
||||
this.createReplaceOperation(patchOperations, 'score', '/score');
|
||||
|
||||
this.handlePatterns(patchOperations, 'notifyServiceInboundPatterns');
|
||||
this.handlePatterns(patchOperations, 'notifyServiceOutboundPatterns');
|
||||
|
||||
|
||||
this.deletedInboundPatterns.forEach(index => {
|
||||
const removeOperation: Operation = {
|
||||
op: 'remove',
|
||||
path: `notifyServiceInboundPatterns[${index}]`
|
||||
};
|
||||
patchOperations.push(removeOperation);
|
||||
});
|
||||
|
||||
this.deletedOutboundPatterns.forEach(index => {
|
||||
const removeOperation: Operation = {
|
||||
op: 'remove',
|
||||
path: `notifyServiceOutboundPatterns[${index}]`
|
||||
};
|
||||
patchOperations.push(removeOperation);
|
||||
});
|
||||
|
||||
return patchOperations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the form by opening the confirmation modal
|
||||
*/
|
||||
onSubmit() {
|
||||
this.openConfirmModal(this.confirmModal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new inbound pattern form group to the array of inbound patterns in the form
|
||||
*/
|
||||
addInboundPattern() {
|
||||
const notifyServiceInboundPatternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
|
||||
notifyServiceInboundPatternsArray.push(this.createInboundPatternFormGroup());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new outbound pattern form group to the array of outbound patterns in the form
|
||||
*/
|
||||
addOutboundPattern() {
|
||||
const notifyServiceOutboundPatternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray;
|
||||
notifyServiceOutboundPatternsArray.push(this.createOutboundPatternFormGroup());
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects an outbound pattern by updating its values based on the provided pattern value and index
|
||||
* @param patternValue - The selected pattern value
|
||||
* @param index - The index of the outbound pattern in the array
|
||||
*/
|
||||
selectOutboundPattern(patternValue: string, index: number): void {
|
||||
const patternArray = (this.formModel.get('notifyServiceOutboundPatterns') as FormArray);
|
||||
patternArray.controls[index].patchValue({pattern: patternValue});
|
||||
patternArray.controls[index].patchValue({patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternValue + '.label')});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects an outbound item filter by updating its value based on the provided filter value and index
|
||||
* @param filterValue - The selected filter value
|
||||
* @param index - The index of the inbound pattern in the array
|
||||
*/
|
||||
selectOutboundItemFilter(filterValue: string, index: number) {
|
||||
const filterArray = (this.formModel.get('notifyServiceOutboundPatterns') as FormArray);
|
||||
filterArray.controls[index].patchValue({constraint: filterValue});
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects an inbound pattern by updating its values based on the provided pattern value and index
|
||||
* @param patternValue - The selected pattern value
|
||||
* @param index - The index of the inbound pattern in the array
|
||||
*/
|
||||
selectInboundPattern(patternValue: string, index: number): void {
|
||||
const patternArray = (this.formModel.get('notifyServiceInboundPatterns') as FormArray);
|
||||
patternArray.controls[index].patchValue({pattern: patternValue});
|
||||
patternArray.controls[index].patchValue({patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternValue + '.label')});
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects an inbound item filter by updating its value based on the provided filter value and index
|
||||
* @param filterValue - The selected filter value
|
||||
* @param index - The index of the inbound pattern in the array
|
||||
*/
|
||||
selectInboundItemFilter(filterValue: string, index: number): void {
|
||||
const filterArray = (this.formModel.get('notifyServiceInboundPatterns') as FormArray);
|
||||
filterArray.controls[index].patchValue({constraint: filterValue});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the automatic property of an inbound pattern at the specified index
|
||||
* @param i - The index of the inbound pattern in the array
|
||||
*/
|
||||
toggleAutomatic(i: number) {
|
||||
const automaticControl = this.formModel.get(`notifyServiceInboundPatterns.${i}.automatic`);
|
||||
if (automaticControl) {
|
||||
automaticControl.markAsTouched();
|
||||
automaticControl.setValue(!automaticControl.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the enabled status of the LDN service by sending a patch request
|
||||
*/
|
||||
toggleEnabled() {
|
||||
const newStatus = !this.formModel.get('enabled').value;
|
||||
|
||||
const patchOperation: Operation = {
|
||||
op: 'replace',
|
||||
path: '/enabled',
|
||||
value: newStatus,
|
||||
};
|
||||
|
||||
this.ldnServicesService.patch(this.service, [patchOperation]).pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe(
|
||||
() => {
|
||||
|
||||
this.formModel.get('enabled').setValue(newStatus);
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the modal
|
||||
*/
|
||||
closeModal() {
|
||||
this.modalRef.close();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a confirmation modal with the specified content
|
||||
* @param content - The content to be displayed in the modal
|
||||
*/
|
||||
openConfirmModal(content) {
|
||||
this.modalRef = this.modalService.open(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a reset form modal with the specified content
|
||||
* @param content - The content to be displayed in the modal
|
||||
*/
|
||||
openResetFormModal(content) {
|
||||
this.modalRef = this.modalService.open(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches the LDN service by retrieving and sending patch operations geenrated in generatePatchOperations()
|
||||
*/
|
||||
patchService() {
|
||||
this.deleteMarkedInboundPatterns();
|
||||
this.deleteMarkedOutboundPatterns();
|
||||
|
||||
const patchOperations = this.generatePatchOperations();
|
||||
this.formModel.markAllAsTouched();
|
||||
|
||||
// If the form is invalid, close the modal and return
|
||||
if (this.formModel.invalid) {
|
||||
this.closeModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const notifyServiceOutboundPatterns = this.formModel.get('notifyServiceOutboundPatterns') as FormArray;
|
||||
const notifyServiceInboundPatterns = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
|
||||
// If no inbound or outbound patterns are specified, close the modal and return
|
||||
// noify the user that no patterns are specified
|
||||
if (
|
||||
(notifyServiceOutboundPatterns.length === 0 && !notifyServiceOutboundPatterns[0]?.value) ||
|
||||
(notifyServiceInboundPatterns.length === 0 && !notifyServiceInboundPatterns[0]?.value)) {
|
||||
this.notificationService.warning(this.translateService.get('ldn-service-notification.created.warning.title'));
|
||||
this.closeModal();
|
||||
return;
|
||||
}
|
||||
|
||||
this.ldnServicesService.patch(this.service, patchOperations).pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe(
|
||||
(rd: RemoteData<LdnService>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.closeModal();
|
||||
this.sendBack();
|
||||
this.notificationService.success(this.translateService.get('admin.registries.services-formats.modify.success.head'),
|
||||
this.translateService.get('admin.registries.services-formats.modify.success.content'));
|
||||
} else {
|
||||
this.notificationService.error(this.translateService.get('admin.registries.services-formats.modify.failure.head'),
|
||||
this.translateService.get('admin.registries.services-formats.modify.failure.content'));
|
||||
this.closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the form and navigates back to the LDN services page
|
||||
*/
|
||||
resetFormAndLeave() {
|
||||
this.sendBack();
|
||||
this.closeModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the specified inbound pattern for deletion
|
||||
* @param index - The index of the inbound pattern in the array
|
||||
*/
|
||||
markForInboundPatternDeletion(index: number) {
|
||||
if (!this.markedForDeletionInboundPattern.includes(index)) {
|
||||
this.markedForDeletionInboundPattern.push(index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmarks the specified inbound pattern for deletion
|
||||
* @param index - The index of the inbound pattern in the array
|
||||
*/
|
||||
unmarkForInboundPatternDeletion(index: number) {
|
||||
const i = this.markedForDeletionInboundPattern.indexOf(index);
|
||||
if (i !== -1) {
|
||||
this.markedForDeletionInboundPattern.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the specified outbound pattern for deletion
|
||||
* @param index - The index of the outbound pattern in the array
|
||||
*/
|
||||
markForOutboundPatternDeletion(index: number) {
|
||||
if (!this.markedForDeletionOutboundPattern.includes(index)) {
|
||||
this.markedForDeletionOutboundPattern.push(index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmarks the specified outbound pattern for deletion
|
||||
* @param index - The index of the outbound pattern in the array
|
||||
*/
|
||||
unmarkForOutboundPatternDeletion(index: number) {
|
||||
const i = this.markedForDeletionOutboundPattern.indexOf(index);
|
||||
if (i !== -1) {
|
||||
this.markedForDeletionOutboundPattern.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes marked inbound patterns from the form model
|
||||
*/
|
||||
deleteMarkedInboundPatterns() {
|
||||
this.markedForDeletionInboundPattern.sort((a, b) => b - a);
|
||||
const patternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
|
||||
|
||||
for (const index of this.markedForDeletionInboundPattern) {
|
||||
if (index >= 0 && index < patternsArray.length) {
|
||||
const patternGroup = patternsArray.at(index) as FormGroup;
|
||||
const patternValue = patternGroup.value;
|
||||
if (patternValue.isNew) {
|
||||
patternsArray.removeAt(index);
|
||||
} else {
|
||||
this.deletedInboundPatterns.push(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.markedForDeletionInboundPattern = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes marked outbound patterns from the form model
|
||||
*/
|
||||
deleteMarkedOutboundPatterns() {
|
||||
this.markedForDeletionOutboundPattern.sort((a, b) => b - a);
|
||||
const patternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray;
|
||||
|
||||
for (const index of this.markedForDeletionOutboundPattern) {
|
||||
if (index >= 0 && index < patternsArray.length) {
|
||||
const patternGroup = patternsArray.at(index) as FormGroup;
|
||||
const patternValue = patternGroup.value;
|
||||
if (patternValue.isNew) {
|
||||
patternsArray.removeAt(index);
|
||||
} else {
|
||||
|
||||
this.deletedOutboundPatterns.push(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.markedForDeletionOutboundPattern = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a replace operation and adds it to the patch operations if the form control is dirty
|
||||
* @param patchOperations - The array to store patch operations
|
||||
* @param formControlName - The name of the form control
|
||||
* @param path - The JSON Patch path for the operation
|
||||
*/
|
||||
private createReplaceOperation(patchOperations: any[], formControlName: string, path: string): void {
|
||||
if (this.formModel.get(formControlName).dirty) {
|
||||
patchOperations.push({
|
||||
op: 'replace',
|
||||
path,
|
||||
value: this.formModel.get(formControlName).value.toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles patterns in the form array, checking if an add or replace operations is required
|
||||
* @param patchOperations - The array to store patch operations
|
||||
* @param formArrayName - The name of the form array
|
||||
*/
|
||||
private handlePatterns(patchOperations: any[], formArrayName: string): void {
|
||||
const patternsArray = this.formModel.get(formArrayName) as FormArray;
|
||||
|
||||
for (let i = 0; i < patternsArray.length; i++) {
|
||||
const patternGroup = patternsArray.at(i) as FormGroup;
|
||||
|
||||
const patternValue = patternGroup.value;
|
||||
if (patternGroup.touched && patternGroup.valid) {
|
||||
delete patternValue?.patternLabel;
|
||||
if (patternValue.isNew) {
|
||||
delete patternValue.isNew;
|
||||
const addOperation = {
|
||||
op: 'add',
|
||||
path: `${formArrayName}/-`,
|
||||
value: patternValue,
|
||||
};
|
||||
patchOperations.push(addOperation);
|
||||
} else {
|
||||
const replaceOperation = {
|
||||
op: 'replace',
|
||||
path: `${formArrayName}[${i}]`,
|
||||
value: patternValue,
|
||||
};
|
||||
patchOperations.push(replaceOperation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates back to the LDN services page
|
||||
*/
|
||||
private sendBack() {
|
||||
this.router.navigateByUrl('admin/ldn/services');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a form group for outbound patterns
|
||||
* @returns The form group for outbound patterns
|
||||
*/
|
||||
private createOutboundPatternFormGroup(): FormGroup {
|
||||
return this.formBuilder.group({
|
||||
pattern: '',
|
||||
patternLabel: this.translateService.instant(this.selectPatternDefaultLabeli18Key),
|
||||
constraint: '',
|
||||
isNew: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a form group for inbound patterns
|
||||
* @returns The form group for inbound patterns
|
||||
*/
|
||||
private createInboundPatternFormGroup(): FormGroup {
|
||||
return this.formBuilder.group({
|
||||
pattern: '',
|
||||
patternLabel: this.translateService.instant(this.selectPatternDefaultLabeli18Key),
|
||||
constraint: '',
|
||||
automatic: false,
|
||||
isNew: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes an existing form group for outbound patterns
|
||||
* @returns The initialized form group for outbound patterns
|
||||
*/
|
||||
private initializeOutboundPatternFormGroup(): FormGroup {
|
||||
return this.formBuilder.group({
|
||||
pattern: '',
|
||||
patternLabel: '',
|
||||
constraint: '',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes an existing form group for inbound patterns
|
||||
* @returns The initialized form group for inbound patterns
|
||||
*/
|
||||
private initializeInboundPatternFormGroup(): FormGroup {
|
||||
return this.formBuilder.group({
|
||||
pattern: '',
|
||||
patternLabel: '',
|
||||
constraint: '',
|
||||
automatic: '',
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,11 +1,21 @@
|
||||
<div class="container">
|
||||
<form (ngSubmit)="onSubmit()" [formGroup]="formModel">
|
||||
<div class="d-flex mb-5">
|
||||
<h2 class="flex-grow-1">{{ 'ldn-create-service.title' | translate }}</h2>
|
||||
<div class="d-flex">
|
||||
<h2 class="flex-grow-1">{{ isNewService ? ('ldn-create-service.title' | translate) : ('ldn-edit-registered-service.title' | translate) }}</h2>
|
||||
</div>
|
||||
<!-- In the name section -->
|
||||
<!-- In the toggle section -->
|
||||
<div class="toggle-switch-container" *ngIf="!isNewService">
|
||||
<label class="status-label font-weight-bold" for="enabled">{{ 'ldn-service-status' | translate }}</label>
|
||||
<div>
|
||||
<input formControlName="enabled" hidden id="enabled" name="enabled" type="checkbox">
|
||||
<div (click)="toggleEnabled()" [class.checked]="formModel.get('enabled').value" class="toggle-switch">
|
||||
<div class="slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- In the Name section -->
|
||||
<div class="mb-5">
|
||||
<label for="name">{{ 'ldn-new-service.form.label.name' | translate }}</label>
|
||||
<label for="name" class="font-weight-bold">{{ 'ldn-new-service.form.label.name' | translate }}</label>
|
||||
<input [class.invalid-field]="formModel.get('name').invalid && formModel.get('name').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.name' | translate" class="form-control"
|
||||
formControlName="name"
|
||||
@@ -19,15 +29,14 @@
|
||||
|
||||
<!-- In the description section -->
|
||||
<div class="mb-5 mt-5 d-flex flex-column">
|
||||
<label for="description">{{ 'ldn-new-service.form.label.description' | translate }}</label>
|
||||
<label for="description" class="font-weight-bold">{{ 'ldn-new-service.form.label.description' | translate }}</label>
|
||||
<textarea [placeholder]="'ldn-new-service.form.placeholder.description' | translate"
|
||||
class="form-control" formControlName="description" id="description" name="description"></textarea>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- In the url section -->
|
||||
<div class="mb-5 mt-5">
|
||||
<label for="url">{{ 'ldn-new-service.form.label.url' | translate }}</label>
|
||||
<label for="url" class="font-weight-bold">{{ 'ldn-new-service.form.label.url' | translate }}</label>
|
||||
<input [class.invalid-field]="formModel.get('url').invalid && formModel.get('url').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.url' | translate" class="form-control"
|
||||
formControlName="url"
|
||||
@@ -39,10 +48,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- In the ldnUrl section -->
|
||||
<div class="mb-5 mt-5">
|
||||
<label for="ldnUrl">{{ 'ldn-new-service.form.label.ldnUrl' | translate }}</label>
|
||||
<label for="ldnUrl" class="font-weight-bold">{{ 'ldn-new-service.form.label.ldnUrl' | translate }}</label>
|
||||
<input [class.invalid-field]="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.ldnUrl' | translate" class="form-control"
|
||||
formControlName="ldnUrl"
|
||||
@@ -53,9 +61,10 @@
|
||||
{{ 'ldn-new-service.form.error.ldnurl' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- In the score section -->
|
||||
<div class="mb-2">
|
||||
<label for="score">{{ 'ldn-new-service.form.label.score' | translate }}</label>
|
||||
<label for="score" class="font-weight-bold">{{ 'ldn-new-service.form.label.score' | translate }}</label>
|
||||
<input [class.invalid-field]="formModel.get('score').invalid && formModel.get('score').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.score' | translate" formControlName="score"
|
||||
id="score"
|
||||
@@ -71,271 +80,160 @@
|
||||
</div>
|
||||
|
||||
<!-- In the Inbound Patterns Labels section -->
|
||||
<div class="row mb-2 mt-5">
|
||||
<div class="row mb-1 mt-5" *ngIf="areControlsInitialized">
|
||||
<div class="col">
|
||||
<label>{{ 'ldn-new-service.form.label.inboundPattern' | translate }} </label>
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.inboundPattern' | translate }} </label>
|
||||
</div>
|
||||
<ng-container *ngIf="!!(formModel.get('notifyServiceInboundPatterns')['controls'][0]?.value?.pattern)">
|
||||
<div class="col">
|
||||
<label>{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<label class="">{{ 'ldn-new-service.form.label.automatic' | translate }}</label>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="col-sm-1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- In the Inbound Patterns section -->
|
||||
<div *ngFor="let patternGroup of formModel.get('notifyServiceInboundPatterns')['controls']; let i = index"
|
||||
formGroupName="notifyServiceInboundPatterns">
|
||||
|
||||
<ng-container [formGroupName]="i">
|
||||
|
||||
|
||||
<div class="row mb-1">
|
||||
<div class="col">
|
||||
<div #inboundPatternDropdown="ngbDropdown" class="w-100" id="additionalInboundPattern{{i}}" ngbDropdown
|
||||
placement="bottom-start">
|
||||
<div class="position-relative right-addon" role="combobox">
|
||||
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
|
||||
ngbDropdownToggle>
|
||||
|
||||
</i>
|
||||
<input
|
||||
(click)="inboundPatternDropdown.open();"
|
||||
[readonly]="true"
|
||||
[value]="selectedInboundPatterns"
|
||||
class="form-control w-100 scrollable-dropdown-input"
|
||||
formControlName="patternLabel"
|
||||
id="inboundPatternDropdownButton"
|
||||
ngbDropdownAnchor
|
||||
type="text"
|
||||
/>
|
||||
<div aria-labelledby="inboundPatternDropdownButton"
|
||||
class="dropdown-menu scrollable-dropdown-menu w-100 "
|
||||
ngbDropdownMenu>
|
||||
<div class="scrollable-menu" role="listbox">
|
||||
<button (click)="selectInboundPattern(pattern, i); $event.stopPropagation()"
|
||||
*ngFor="let pattern of inboundPatterns"
|
||||
[title]="'ldn-service.form.pattern.' + pattern + '.description' | translate"
|
||||
class="dropdown-item collection-item text-truncate w-100"
|
||||
ngbDropdownItem
|
||||
type="button">
|
||||
<div>{{ 'ldn-service.form.pattern.' + pattern + '.label' | translate }}</div>
|
||||
<div
|
||||
class="small-text">{{ 'ldn-service.form.pattern.' + pattern + '.description' | translate }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col">
|
||||
<ng-container
|
||||
*ngIf="!!(formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern)">
|
||||
<div #inboundItemfilterDropdown="ngbDropdown" class="w-100" id="constraint{{i}}" ngbDropdown
|
||||
placement="bottom-start">
|
||||
<div class="position-relative right-addon" role="combobox">
|
||||
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
|
||||
ngbDropdownToggle></i>
|
||||
<input
|
||||
(click)="inboundItemfilterDropdown.open();"
|
||||
[readonly]="true"
|
||||
[value]="selectedInboundItemfilters"
|
||||
class="form-control w-100 scrollable-dropdown-input"
|
||||
formControlName="constraint"
|
||||
id="inboundItemfilterDropdown"
|
||||
ngbDropdownAnchor
|
||||
type="text"
|
||||
/>
|
||||
<div aria-labelledby="inboundItemfilterDropdownButton"
|
||||
class="dropdown-menu scrollable-dropdown-menu w-100 "
|
||||
ngbDropdownMenu>
|
||||
<div class="scrollable-menu" role="listbox">
|
||||
<button (click)="selectInboundItemFilter('', i); $event.stopPropagation()"
|
||||
class="dropdown-item collection-item text-truncate w-100" ngbDropdownItem type="button">
|
||||
<span> {{'ldn-service.control-constaint-select-none' | translate}} </span>
|
||||
</button>
|
||||
<button (click)="selectInboundItemFilter(constraint.id, i); $event.stopPropagation() "
|
||||
*ngFor="let constraint of (itemfiltersRD$ | async)?.payload?.page; let internalIndex = index"
|
||||
class="dropdown-item collection-item text-truncate w-100"
|
||||
ngbDropdownItem
|
||||
type="button">
|
||||
<div>{{ constraint.id }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div
|
||||
[style.visibility]="formModel.get('notifyServiceInboundPatterns')['controls'][i]?.value?.pattern ? 'visible' : 'hidden'"
|
||||
class="col-sm-1">
|
||||
<input formControlName="automatic" hidden id="automatic{{i}}" name="automatic{{i}}"
|
||||
type="checkbox">
|
||||
<div (click)="toggleAutomatic(i)"
|
||||
[class.checked]="formModel.get('notifyServiceInboundPatterns.' + i + '.automatic').value"
|
||||
class="toggle-switch">
|
||||
<div class="slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-1">
|
||||
<button (click)="removeInboundPattern(i)" class="btn btn-outline-dark trash-button">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<span (click)="addInboundPattern()"
|
||||
class="add-pattern-link mb-4">{{ 'ldn-new-service.form.label.addPattern' | translate }}</span>
|
||||
|
||||
|
||||
<!-- In the Outbound Patterns Labels section -->
|
||||
<div class="row mb-1 mt-5">
|
||||
<div class="col">
|
||||
<label>{{ 'ldn-new-service.form.label.outboundPattern' | translate }}</label>
|
||||
</div>
|
||||
<ng-container *ngIf="!!(formModel.get('notifyServiceOutboundPatterns')['controls'][0]?.value?.pattern)">
|
||||
<div class="col">
|
||||
<label class="">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.automatic' | translate }}</label>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="col-sm-2">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- In the Outbound Patterns section -->
|
||||
<div *ngFor="let patternGroup of formModel.get('notifyServiceOutboundPatterns')['controls']; let i = index"
|
||||
formGroupName="notifyServiceOutboundPatterns">
|
||||
<!-- In the Inbound Patterns section -->
|
||||
<div *ngIf="areControlsInitialized">
|
||||
<div *ngFor="let patternGroup of formModel.get('notifyServiceInboundPatterns')['controls']; let i = index"
|
||||
[class.marked-for-deletion]="markedForDeletionInboundPattern.includes(i)"
|
||||
formGroupName="notifyServiceInboundPatterns">
|
||||
|
||||
<ng-container [formGroupName]="i">
|
||||
<ng-container [formGroupName]="i">
|
||||
|
||||
<div class="row mb-1">
|
||||
<div class="col">
|
||||
<div #outboundPatternDropdown="ngbDropdown" class="w-100" id="additionalOutboundPattern{{i}}"
|
||||
ngbDropdown
|
||||
placement="bottom-start">
|
||||
<div class="position-relative right-addon" role="combobox">
|
||||
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
|
||||
ngbDropdownToggle></i>
|
||||
<input
|
||||
(click)="outboundPatternDropdown.open();"
|
||||
[readonly]="true"
|
||||
[value]="selectedOutboundPatterns"
|
||||
class="form-control w-100 scrollable-dropdown-input"
|
||||
formControlName="patternLabel"
|
||||
id="outboundPatternDropdownButton"
|
||||
ngbDropdownAnchor
|
||||
type="text"
|
||||
/>
|
||||
<div aria-labelledby="outboundPatternDropdownButton"
|
||||
class="dropdown-menu scrollable-dropdown-menu w-100 "
|
||||
ngbDropdownMenu>
|
||||
<div class="scrollable-menu" role="listbox">
|
||||
<button (click)="selectOutboundPattern(pattern, i); $event.stopPropagation()"
|
||||
*ngFor="let pattern of outboundPatterns"
|
||||
[title]="'ldn-service.form.pattern.' + pattern + '.description' | translate"
|
||||
class="dropdown-item collection-item text-truncate w-100"
|
||||
ngbDropdownItem
|
||||
type="button">
|
||||
<div>{{ 'ldn-service.form.pattern.' + pattern + '.label' | translate }}</div>
|
||||
<div
|
||||
class="small-text">{{ 'ldn-service.form.pattern.' + pattern + '.description' | translate }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<ng-container
|
||||
*ngIf="!!(formModel.get('notifyServiceOutboundPatterns')['controls'][i].value.pattern)">
|
||||
<div #outboundItemfilterDropdown="ngbDropdown" class="w-100" id="constraint{{i}}"
|
||||
ngbDropdown
|
||||
placement="bottom-start">
|
||||
<div class="row mb-1 align-items-center">
|
||||
<div class="col">
|
||||
<div #inboundPatternDropdown="ngbDropdown" class="w-80" display="dynamic"
|
||||
id="additionalInboundPattern{{i}}"
|
||||
ngbDropdown placement="top-start">
|
||||
<div class="position-relative right-addon" role="combobox">
|
||||
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
|
||||
ngbDropdownToggle></i>
|
||||
<input
|
||||
(click)="outboundItemfilterDropdown.open();"
|
||||
(click)="inboundPatternDropdown.open();"
|
||||
[readonly]="true"
|
||||
[value]="selectedOutboundItemfilters"
|
||||
class="form-control w-100 scrollable-dropdown-input"
|
||||
formControlName="constraint"
|
||||
id="outboundItemfilterDropdown"
|
||||
[value]="selectedInboundPatterns"
|
||||
class="form-control w-80 scrollable-dropdown-input"
|
||||
formControlName="patternLabel"
|
||||
id="inboundPatternDropdownButton"
|
||||
ngbDropdownAnchor
|
||||
type="text"
|
||||
/>
|
||||
<div aria-labelledby="outboundItemfilterDropdownButton"
|
||||
class="dropdown-menu scrollable-dropdown-menu w-100 "
|
||||
<div aria-labelledby="inboundPatternDropdownButton"
|
||||
class="dropdown-menu dropdown-menu-top w-100 "
|
||||
ngbDropdownMenu>
|
||||
<div class="scrollable-menu" role="listbox">
|
||||
<button (click)="selectOutboundItemFilter('', i); $event.stopPropagation()"
|
||||
class="dropdown-item collection-item text-truncate w-100" ngbDropdownItem type="button">
|
||||
<span> {{'ldn-service.control-constaint-select-none' | translate}} </span>
|
||||
</button>
|
||||
<button (click)="selectOutboundItemFilter(constraint.id, i); $event.stopPropagation()"
|
||||
*ngFor="let constraint of (itemfiltersRD$ | async)?.payload?.page; let internalIndex = index"
|
||||
<button (click)="selectInboundPattern(pattern, i); $event.stopPropagation()"
|
||||
*ngFor="let pattern of inboundPatterns; let internalIndex = index"
|
||||
[title]="'ldn-service.form.pattern.' + pattern + '.description' | translate"
|
||||
class="dropdown-item collection-item text-truncate w-100"
|
||||
ngbDropdownItem
|
||||
type="button">
|
||||
<div>{{ constraint.id }}</div>
|
||||
<div>{{ 'ldn-service.form.pattern.' + pattern + '.label' | translate }}</div>
|
||||
<div
|
||||
class="small-text">{{ 'ldn-service.form.pattern.' + pattern + '.description' | translate }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<ng-container
|
||||
*ngIf="!!(formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern)">
|
||||
<div #inboundItemfilterDropdown="ngbDropdown" class="w-100" id="constraint{{i}}" ngbDropdown
|
||||
placement="top-start">
|
||||
<div class="position-relative right-addon" role="combobox">
|
||||
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
|
||||
ngbDropdownToggle></i>
|
||||
<input
|
||||
(click)="inboundItemfilterDropdown.open();"
|
||||
[readonly]="true"
|
||||
[value]="selectedInboundItemfilters"
|
||||
class="form-control w-100 scrollable-dropdown-input"
|
||||
formControlName="constraint"
|
||||
id="inboundItemfilterDropdown"
|
||||
ngbDropdownAnchor
|
||||
type="text"
|
||||
/>
|
||||
<div aria-labelledby="inboundItemfilterDropdownButton"
|
||||
class="dropdown-menu scrollable-dropdown-menu w-100 "
|
||||
ngbDropdownMenu>
|
||||
<div class="scrollable-menu" role="listbox">
|
||||
<button (click)="selectInboundItemFilter('', i); $event.stopPropagation()"
|
||||
class="dropdown-item collection-item text-truncate w-100" ngbDropdownItem type="button">
|
||||
<span> {{'ldn-service.control-constaint-select-none' | translate}} </span>
|
||||
</button>
|
||||
<button (click)="selectInboundItemFilter(constraint.id, i); $event.stopPropagation()"
|
||||
*ngFor="let constraint of (itemfiltersRD$ | async)?.payload?.page; let internalIndex = index"
|
||||
class="dropdown-item collection-item text-truncate w-100"
|
||||
ngbDropdownItem
|
||||
type="button">
|
||||
<div>{{ constraint.id }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div [style.visibility]="'hidden'" class="col-sm-1">
|
||||
<input hidden id="automatic{{i}}" name="automatic{{i}}"
|
||||
type="checkbox">
|
||||
<div
|
||||
class="toggle-switch">
|
||||
<div class="slider"></div>
|
||||
[style.visibility]="formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern ? 'visible' : 'hidden'"
|
||||
class="col-sm-1">
|
||||
<input formControlName="automatic" hidden id="automatic{{i}}" name="automatic{{i}}"
|
||||
type="checkbox">
|
||||
<div (click)="toggleAutomatic(i)"
|
||||
[class.checked]="formModel.get('notifyServiceInboundPatterns.' + i + '.automatic').value"
|
||||
class="toggle-switch">
|
||||
<div class="slider"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-sm-2">
|
||||
<div class="btn-group">
|
||||
<button (click)="markForInboundPatternDeletion(i)" class="btn btn-outline-dark trash-button"
|
||||
type="button">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
|
||||
|
||||
<button (click)="unmarkForInboundPatternDeletion(i)"
|
||||
*ngIf="markedForDeletionInboundPattern.includes(i)"
|
||||
class="btn btn-warning "
|
||||
type="button">
|
||||
<i class="fas fa-undo"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-1">
|
||||
<button (click)="removeOutboundPattern(i)" class="btn btn-outline-dark trash-button">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div (click)="addOutboundPattern()"
|
||||
class="add-pattern-link mb-4">{{ 'ldn-new-service.form.label.addPattern' | translate }}
|
||||
</div>
|
||||
<span (click)="addInboundPattern()"
|
||||
class="add-pattern-link mb-2">{{ 'ldn-new-service.form.label.addPattern' | translate }}</span>
|
||||
|
||||
<div class="submission-form-footer my-1 position-sticky d-flex justify-content-between" role="group">
|
||||
<button (click)="this.openResetFormModal(this.resetFormModal)" class="btn btn-danger" type="button">
|
||||
<button (click)="openResetFormModal(resetFormModal)" class="btn btn-danger" type="button">
|
||||
<span><i class="fas fa-trash"></i> {{ 'submission.general.discard.submit' | translate }}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary" type="submit">
|
||||
<span><i class="fas fa-save"></i> {{ 'ldn-new-service.form.label.submit' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<ng-template #confirmModal>
|
||||
<div class="modal-header">
|
||||
<h4>{{'service.overview.create.modal' | translate }}</h4>
|
||||
<h4 *ngIf="!isNewService">{{'service.overview.edit.modal' | translate }}</h4>
|
||||
<h4 *ngIf="isNewService">{{'service.overview.create.modal' | translate }}</h4>
|
||||
<button (click)="closeModal()" aria-label="Close"
|
||||
class="close" type="button">
|
||||
<span aria-hidden="true">×</span>
|
||||
@@ -343,23 +241,36 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<span>
|
||||
<div *ngIf="!isNewService">
|
||||
{{ 'service.overview.edit.body' | translate }}
|
||||
</div>
|
||||
<span *ngIf="isNewService">
|
||||
{{ 'service.overview.create.body' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button (click)="closeModal()" class="btn btn-danger mr-2 "
|
||||
id="delete-confirm">{{ 'service.refuse.create' | translate }}
|
||||
</button>
|
||||
<button (click)="createService()"
|
||||
class="btn btn-primary">{{ 'service.confirm.create' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div *ngIf="!isNewService">
|
||||
<button (click)="closeModal()" class="btn btn-danger mr-2"
|
||||
id="delete-confirm-edit">{{ 'service.detail.return' | translate }}
|
||||
</button>
|
||||
<button *ngIf="!isNewService" (click)="patchService()"
|
||||
class="btn btn-primary">{{ 'service.detail.update' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="isNewService">
|
||||
<button (click)="closeModal()" class="btn btn-danger mr-2 "
|
||||
id="delete-confirm-new">{{ 'service.refuse.create' | translate }}
|
||||
</button>
|
||||
<button (click)="createService()"
|
||||
class="btn btn-primary">{{ 'service.confirm.create' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #resetFormModal>
|
||||
<div class="modal-header">
|
||||
<h4 class="text-danger">{{'service.overview.reset-form.modal' | translate }}</h4>
|
||||
<h4 class="text-danger">{{'service.overview.reset-form.modal' | translate }}</h4>
|
||||
<button (click)="closeModal()" aria-label="Close"
|
||||
class="close" type="button">
|
||||
<span aria-hidden="true">×</span>
|
||||
@@ -370,17 +281,15 @@
|
||||
<span>
|
||||
{{ 'service.overview.reset-form.body' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button (click)="closeModal()"
|
||||
class="btn btn-primary mr-2"
|
||||
id="reset-confirm">{{ 'service.overview.reset-form.reset-return' | translate }}
|
||||
</button>
|
||||
<button (click)="resetFormAndLeave()" class="btn btn-danger"
|
||||
id="reset-delete">{{ 'service.overview.reset-form.reset-confirm' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button (click)="closeModal()"
|
||||
class="btn btn-primary"
|
||||
id="reset-confirm">{{ 'service.overview.reset-form.reset-return' | translate }}
|
||||
</button>
|
||||
<button (click)="resetFormAndLeave()" class="mr-2 btn btn-danger"
|
||||
id="reset-delete">{{ 'service.overview.reset-form.reset-confirm' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
|
||||
|
@@ -6,10 +6,6 @@ form {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
select {
|
||||
max-width: 100%;
|
||||
@@ -43,11 +39,6 @@ textarea {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.small-text {
|
||||
font-size: 0.7em;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.status-checkbox {
|
||||
margin-top: 5px;
|
||||
}
|
||||
@@ -110,8 +101,27 @@ textarea {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.small-text {
|
||||
font-size: 0.7em;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
cursor: pointer;
|
||||
margin-top: 3px;
|
||||
margin-right: 3px
|
||||
}
|
||||
|
||||
.label-box {
|
||||
margin-left: 11px;
|
||||
}
|
||||
|
||||
.label-box-2 {
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
.label-box-3 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.submission-form-footer {
|
||||
@@ -119,10 +129,15 @@ textarea {
|
||||
bottom: 0;
|
||||
background-color: var(--bs-gray-400);
|
||||
padding: calc(var(--bs-spacer) / 2);
|
||||
z-index: calc(var(--ds-submission-footer-z-index) + 1);
|
||||
}
|
||||
|
||||
.marked-for-deletion {
|
||||
background-color: lighten($red, 30%);
|
||||
}
|
||||
|
||||
.dropdown-menu-top, .scrollable-dropdown-menu {
|
||||
z-index: var(--ds-submission-footer-z-index);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -1,30 +1,30 @@
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {NgbDropdownModule, NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {LdnServiceFormComponent} from './ldn-service-form.component';
|
||||
import {ChangeDetectorRef, EventEmitter} from '@angular/core';
|
||||
import {FormBuilder, ReactiveFormsModule} from '@angular/forms';
|
||||
import {RouterTestingModule} from '@angular/router/testing';
|
||||
import {NgbDropdownModule, NgbModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {TranslateModule, TranslateService} from '@ngx-translate/core';
|
||||
import {PaginationService} from 'ngx-pagination';
|
||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||
import {LdnItemfiltersService} from '../ldn-services-data/ldn-itemfilters-data.service';
|
||||
import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service';
|
||||
import {NotificationsService} from 'src/app/shared/notifications/notifications.service';
|
||||
import {Router} from '@angular/router';
|
||||
import {RouterStub} from 'src/app/shared/testing/router.stub';
|
||||
import {createPaginatedList} from 'src/app/shared/testing/utils.test';
|
||||
import {Itemfilter} from '../ldn-services-model/ldn-service-itemfilters';
|
||||
import {createSuccessfulRemoteDataObject$} from 'src/app/shared/remote-data.utils';
|
||||
import {RouterStub} from '../../../shared/testing/router.stub';
|
||||
import {MockActivatedRoute} from '../../../shared/mocks/active-router.mock';
|
||||
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service.stub';
|
||||
import {of} from 'rxjs';
|
||||
import {EventEmitter} from '@angular/core';
|
||||
import {RouteService} from '../../../core/services/route.service';
|
||||
import {provideMockStore} from '@ngrx/store/testing';
|
||||
|
||||
describe('LdnServiceFormComponent', () => {
|
||||
describe('LdnServiceFormEditComponent', () => {
|
||||
let component: LdnServiceFormComponent;
|
||||
let fixture: ComponentFixture<LdnServiceFormComponent>;
|
||||
|
||||
let ldnServicesService: any;
|
||||
let ldnItemfiltersService: any;
|
||||
let notificationsService: any;
|
||||
|
||||
const itemFiltersRdPL$ = createSuccessfulRemoteDataObject$(createPaginatedList([new Itemfilter()]));
|
||||
let cdRefStub: any;
|
||||
let modalService: any;
|
||||
|
||||
const translateServiceStub = {
|
||||
get: () => of('translated-text'),
|
||||
@@ -32,53 +32,45 @@ describe('LdnServiceFormComponent', () => {
|
||||
onLangChange: new EventEmitter(),
|
||||
onTranslationChange: new EventEmitter(),
|
||||
onDefaultLangChange: new EventEmitter()
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
ldnItemfiltersService = jasmine.createSpyObj('ldnItemfiltersService', {
|
||||
findAll: jasmine.createSpy('findAll'),
|
||||
});
|
||||
|
||||
ldnServicesService = jasmine.createSpyObj('ldnServicesService', {
|
||||
create: jasmine.createSpy('create'),
|
||||
});
|
||||
|
||||
notificationsService = jasmine.createSpyObj('notificationsService', {
|
||||
success: jasmine.createSpy('success'),
|
||||
error: jasmine.createSpy('error'),
|
||||
ldnServicesService = {
|
||||
update: () => ({}),
|
||||
};
|
||||
ldnItemfiltersService = {
|
||||
findAll: () => of(['item1', 'item2']),
|
||||
};
|
||||
cdRefStub = Object.assign({
|
||||
detectChanges: () => fixture.detectChanges()
|
||||
});
|
||||
modalService = {
|
||||
open: () => {/*comment*/
|
||||
}
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
RouterTestingModule,
|
||||
NgbModalModule,
|
||||
TranslateModule.forRoot(),
|
||||
NgbDropdownModule
|
||||
],
|
||||
imports: [ReactiveFormsModule, TranslateModule.forRoot(), NgbDropdownModule],
|
||||
declarations: [LdnServiceFormComponent],
|
||||
providers: [
|
||||
{provide: LdnItemfiltersService, useValue: ldnItemfiltersService},
|
||||
{provide: LdnServicesService, useValue: ldnServicesService},
|
||||
{provide: NotificationsService, useValue: notificationsService},
|
||||
{provide: TranslateService, useValue: translateServiceStub},
|
||||
{provide: LdnItemfiltersService, useValue: ldnItemfiltersService},
|
||||
{provide: Router, useValue: new RouterStub()},
|
||||
{
|
||||
provide: NgbModal, useValue: {
|
||||
open: () => {/*comment*/
|
||||
}
|
||||
}
|
||||
},
|
||||
FormBuilder
|
||||
],
|
||||
declarations: [LdnServiceFormComponent]
|
||||
{provide: ActivatedRoute, useValue: new MockActivatedRoute()},
|
||||
{provide: ChangeDetectorRef, useValue: cdRefStub},
|
||||
{provide: NgbModal, useValue: modalService},
|
||||
{provide: NotificationsService, useValue: NotificationsServiceStub},
|
||||
{provide: TranslateService, useValue: translateServiceStub},
|
||||
{provide: PaginationService, useValue: {}},
|
||||
FormBuilder,
|
||||
RouteService,
|
||||
provideMockStore({}),
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LdnServiceFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
ldnItemfiltersService.findAll.and.returnValue(itemFiltersRdPL$);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
|
@@ -1,27 +1,38 @@
|
||||
import {ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, TemplateRef, ViewChild} from '@angular/core';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
TemplateRef,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import {FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service';
|
||||
import {notifyPatterns} from '../ldn-services-patterns/ldn-service-coar-patterns';
|
||||
import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type';
|
||||
import {animate, state, style, transition, trigger} from '@angular/animations';
|
||||
import {getFirstCompletedRemoteData} from '../../../core/shared/operators';
|
||||
import {RemoteData} from '../../../core/data/remote-data';
|
||||
import {LdnService} from '../ldn-services-model/ldn-services.model';
|
||||
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {PaginatedList} from '../../../core/data/paginated-list.model';
|
||||
import {Itemfilter} from '../ldn-services-model/ldn-service-itemfilters';
|
||||
import {Observable} from 'rxjs';
|
||||
import {FindListOptions} from '../../../core/data/find-list-options.model';
|
||||
import {PaginationComponentOptions} from '../../../shared/pagination/pagination-component-options.model';
|
||||
import {LdnService} from '../ldn-services-model/ldn-services.model';
|
||||
import {RemoteData} from 'src/app/core/data/remote-data';
|
||||
import {Operation} from 'fast-json-patch';
|
||||
import {getFirstCompletedRemoteData} from '../../../core/shared/operators';
|
||||
import {LdnItemfiltersService} from '../ldn-services-data/ldn-itemfilters-data.service';
|
||||
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {Itemfilter} from '../ldn-services-model/ldn-service-itemfilters';
|
||||
import {PaginatedList} from '../../../core/data/paginated-list.model';
|
||||
import {combineLatestWith, Observable, Subscription} from 'rxjs';
|
||||
import {PaginationService} from '../../../core/pagination/pagination.service';
|
||||
import {FindListOptions} from '../../../core/data/find-list-options.model';
|
||||
import {NotifyServicePattern} from '../ldn-services-model/ldn-service-patterns.model';
|
||||
|
||||
|
||||
/**
|
||||
* Angular component representing the form for creating or editing LDN services.
|
||||
* This component handles the creation, validation, and submission of LDN service data.
|
||||
* Component for editing LDN service through a form that allows to create or edit the properties of a service
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-ldn-service-form',
|
||||
@@ -35,108 +46,93 @@ import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class LdnServiceFormComponent implements OnInit {
|
||||
export class LdnServiceFormComponent implements OnInit, OnDestroy {
|
||||
formModel: FormGroup;
|
||||
@ViewChild('confirmModal', {static: true}) confirmModal: TemplateRef<any>;
|
||||
@ViewChild('resetFormModal', {static: true}) resetFormModal: TemplateRef<any>;
|
||||
|
||||
public inboundPatterns: string[] = notifyPatterns;
|
||||
public outboundPatterns: string[] = notifyPatterns;
|
||||
public isNewService: boolean;
|
||||
public areControlsInitialized: boolean;
|
||||
itemfiltersRD$: Observable<RemoteData<PaginatedList<Itemfilter>>>;
|
||||
config: FindListOptions = Object.assign(new FindListOptions(), {
|
||||
elementsPerPage: 20
|
||||
});
|
||||
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'po',
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
@Input() public name: string;
|
||||
@Input() public description: string;
|
||||
@Input() public url: string;
|
||||
@Input() public score: string;
|
||||
@Input() public ldnUrl: string;
|
||||
@Input() public score: number;
|
||||
@Input() public inboundPattern: string;
|
||||
@Input() public outboundPattern: string;
|
||||
@Input() public constraint: string;
|
||||
@Input() public automatic: boolean;
|
||||
@Input() public headerKey: string;
|
||||
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
||||
@Output() cancelForm: EventEmitter<any> = new EventEmitter();
|
||||
selectedOutboundPatterns: string[];
|
||||
markedForDeletionInboundPattern: number[] = [];
|
||||
selectedInboundPatterns: string[];
|
||||
selectedInboundItemfilters: string[];
|
||||
selectedOutboundItemfilters: string[];
|
||||
hasInboundPattern: boolean;
|
||||
hasOutboundPattern: boolean;
|
||||
isScoreValid: boolean;
|
||||
protected serviceId: string;
|
||||
private deletedInboundPatterns: number[] = [];
|
||||
private modalRef: any;
|
||||
private service: LdnService;
|
||||
private selectPatternDefaultLabeli18Key = 'ldn-service.form.label.placeholder.default-select';
|
||||
private routeSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private ldnServicesService: LdnServicesService,
|
||||
protected ldnServicesService: LdnServicesService,
|
||||
private ldnItemfiltersService: LdnItemfiltersService,
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router,
|
||||
private notificationsService: NotificationsService,
|
||||
private translateService: TranslateService,
|
||||
private route: ActivatedRoute,
|
||||
private cdRef: ChangeDetectorRef,
|
||||
protected modalService: NgbModal,
|
||||
private notificationService: NotificationsService,
|
||||
private translateService: TranslateService,
|
||||
protected paginationService: PaginationService
|
||||
) {
|
||||
|
||||
this.formModel = this.formBuilder.group({
|
||||
enabled: true,
|
||||
id: [''],
|
||||
name: ['', Validators.required],
|
||||
description: [''],
|
||||
url: ['', Validators.required],
|
||||
score: ['', [Validators.required, Validators.pattern('^0*(\.[0-9]+)?$|^1(\.0+)?$')]],
|
||||
ldnUrl: ['', Validators.required],
|
||||
inboundPattern: [''],
|
||||
outboundPattern: [''],
|
||||
score: ['', [Validators.required, Validators.pattern('^0*(\.[0-9]+)?$|^1(\.0+)?$')]], inboundPattern: [''],
|
||||
constraintPattern: [''],
|
||||
notifyServiceInboundPatterns: this.formBuilder.array([this.createInboundPatternFormGroup()]),
|
||||
notifyServiceOutboundPatterns: this.formBuilder.array([this.createOutboundPatternFormGroup()]),
|
||||
enabled: [''],
|
||||
type: LDN_SERVICE.value,
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.routeSubscription = this.route.params.pipe(
|
||||
combineLatestWith(this.route.url)
|
||||
).subscribe(([params, segment]) => {
|
||||
this.serviceId = params.serviceId;
|
||||
this.isNewService = segment[0].path === 'new';
|
||||
this.formModel.addControl('notifyServiceInboundPatterns', this.formBuilder.array([this.createInboundPatternFormGroup()]));
|
||||
this.areControlsInitialized = true;
|
||||
if (this.serviceId && !this.isNewService) {
|
||||
this.fetchServiceData(this.serviceId);
|
||||
}
|
||||
});
|
||||
this.setItemfilters();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.routeSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the item filters by fetching and observing the paginated list of item filters.
|
||||
* Sets item filters using LDN item filters service
|
||||
*/
|
||||
setItemfilters() {
|
||||
this.itemfiltersRD$ = this.ldnItemfiltersService.findAll().pipe(
|
||||
getFirstCompletedRemoteData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the form submission by opening the confirmation modal.
|
||||
*/
|
||||
onSubmit() {
|
||||
this.openConfirmModal(this.confirmModal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the confirmation modal.
|
||||
*
|
||||
* @param {any} content - The content of the modal.
|
||||
*/
|
||||
openConfirmModal(content) {
|
||||
this.modalRef = this.modalService.open(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the reset form modal.
|
||||
*
|
||||
* @param {any} content - The content of the modal.
|
||||
*/
|
||||
openResetFormModal(content) {
|
||||
this.modalRef = this.modalService.open(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the creation of an LDN service by retrieving and validating form fields,
|
||||
* and submitting the form data to the LDN services endpoint.
|
||||
@@ -150,17 +146,16 @@ export class LdnServiceFormComponent implements OnInit {
|
||||
const ldnUrl = this.formModel.get('ldnUrl').value;
|
||||
|
||||
const hasInboundPattern = this.checkPatterns(this.formModel.get('notifyServiceInboundPatterns') as FormArray);
|
||||
const hasOutboundPattern = this.checkPatterns(this.formModel.get('notifyServiceOutboundPatterns') as FormArray);
|
||||
|
||||
if (!name || !url || !ldnUrl || !score || this.formModel.get('score').invalid) {
|
||||
if (!name || !url || !ldnUrl || (!score && score !== 0) || this.formModel.get('score').invalid) {
|
||||
this.closeModal();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasInboundPattern || !hasOutboundPattern) {
|
||||
this.notificationsService.warning(this.translateService.get('ldn-service-notification.created.warning.title'));
|
||||
this.closeModal();
|
||||
return;
|
||||
if (!hasInboundPattern) {
|
||||
this.notificationService.warning(this.translateService.get('ldn-service-notification.created.warning.title'));
|
||||
this.closeModal();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -172,15 +167,7 @@ export class LdnServiceFormComponent implements OnInit {
|
||||
return rest;
|
||||
});
|
||||
|
||||
this.formModel.value.notifyServiceOutboundPatterns = this.formModel.value.notifyServiceOutboundPatterns.map((pattern: {
|
||||
pattern: string;
|
||||
patternLabel: string
|
||||
}) => {
|
||||
const {patternLabel, ...rest} = pattern;
|
||||
return rest;
|
||||
});
|
||||
|
||||
const values = this.formModel.value;
|
||||
const values = {...this.formModel.value, enabled: true};
|
||||
|
||||
const ldnServiceData = this.ldnServicesService.create(values);
|
||||
|
||||
@@ -188,13 +175,13 @@ export class LdnServiceFormComponent implements OnInit {
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe((rd: RemoteData<LdnService>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.notificationsService.success(this.translateService.get('ldn-service-notification.created.success.title'),
|
||||
this.notificationService.success(this.translateService.get('ldn-service-notification.created.success.title'),
|
||||
this.translateService.get('ldn-service-notification.created.success.body'));
|
||||
|
||||
this.sendBack();
|
||||
this.closeModal();
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get('ldn-service-notification.created.failure.title'),
|
||||
this.notificationService.error(this.translateService.get('ldn-service-notification.created.failure.title'),
|
||||
this.translateService.get('ldn-service-notification.created.failure.body'));
|
||||
this.closeModal();
|
||||
}
|
||||
@@ -218,23 +205,92 @@ export class LdnServiceFormComponent implements OnInit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the currently open modal and returns to the services directory..
|
||||
* Fetches LDN service data by ID and updates the form
|
||||
* @param serviceId - The ID of the LDN service
|
||||
*/
|
||||
resetFormAndLeave() {
|
||||
this.sendBack();
|
||||
this.closeModal();
|
||||
fetchServiceData(serviceId: string): void {
|
||||
this.ldnServicesService.findById(serviceId).pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe(
|
||||
(data: RemoteData<LdnService>) => {
|
||||
if (data.hasSucceeded) {
|
||||
this.service = data.payload;
|
||||
|
||||
this.formModel.patchValue({
|
||||
id: this.service.id,
|
||||
name: this.service.name,
|
||||
description: this.service.description,
|
||||
url: this.service.url,
|
||||
score: this.service.score, ldnUrl: this.service.ldnUrl,
|
||||
type: this.service.type,
|
||||
enabled: this.service.enabled
|
||||
});
|
||||
this.filterPatternObjectsAndPickLabel('notifyServiceInboundPatterns');
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the currently open modal and triggers change detection.
|
||||
* Filters pattern objects, initializes form groups, assigns labels, and adds them to the specified form array so the correct string is shown in the dropdown..
|
||||
* @param formArrayName - The name of the form array to be populated
|
||||
*/
|
||||
closeModal() {
|
||||
this.modalRef.close();
|
||||
this.cdRef.detectChanges();
|
||||
filterPatternObjectsAndPickLabel(formArrayName: string) {
|
||||
const PatternsArray = this.formModel.get(formArrayName) as FormArray;
|
||||
PatternsArray.clear();
|
||||
let servicesToUse;
|
||||
servicesToUse = this.service.notifyServiceInboundPatterns;
|
||||
|
||||
servicesToUse.forEach((patternObj: NotifyServicePattern) => {
|
||||
let patternFormGroup;
|
||||
patternFormGroup = this.initializeInboundPatternFormGroup();
|
||||
const newPatternObjWithLabel = Object.assign(new NotifyServicePattern(), {
|
||||
...patternObj,
|
||||
patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternObj?.pattern + '.label')
|
||||
});
|
||||
patternFormGroup.patchValue(newPatternObjWithLabel);
|
||||
|
||||
PatternsArray.push(patternFormGroup);
|
||||
this.cdRef.detectChanges();
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new inbound pattern form group to the notifyServiceInboundPatterns form array.
|
||||
* Generates an array of patch operations based on form changes
|
||||
* @returns Array of patch operations
|
||||
*/
|
||||
generatePatchOperations(): any[] {
|
||||
const patchOperations: any[] = [];
|
||||
|
||||
this.createReplaceOperation(patchOperations, 'name', '/name');
|
||||
this.createReplaceOperation(patchOperations, 'description', '/description');
|
||||
this.createReplaceOperation(patchOperations, 'ldnUrl', '/ldnurl');
|
||||
this.createReplaceOperation(patchOperations, 'url', '/url');
|
||||
this.createReplaceOperation(patchOperations, 'score', '/score');
|
||||
|
||||
this.handlePatterns(patchOperations, 'notifyServiceInboundPatterns');
|
||||
this.deletedInboundPatterns.forEach(index => {
|
||||
const removeOperation: Operation = {
|
||||
op: 'remove',
|
||||
path: `notifyServiceInboundPatterns[${index}]`
|
||||
};
|
||||
patchOperations.push(removeOperation);
|
||||
});
|
||||
|
||||
return patchOperations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the form by opening the confirmation modal
|
||||
*/
|
||||
onSubmit() {
|
||||
this.openConfirmModal(this.confirmModal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new inbound pattern form group to the array of inbound patterns in the form
|
||||
*/
|
||||
addInboundPattern() {
|
||||
const notifyServiceInboundPatternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
|
||||
@@ -242,39 +298,29 @@ export class LdnServiceFormComponent implements OnInit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the inbound pattern form group at the specified index from the notifyServiceInboundPatterns form array.
|
||||
*
|
||||
* @param {number} index - The index of the inbound pattern form group to remove.
|
||||
* @memberof LdnServiceFormComponent
|
||||
* Selects an inbound pattern by updating its values based on the provided pattern value and index
|
||||
* @param patternValue - The selected pattern value
|
||||
* @param index - The index of the inbound pattern in the array
|
||||
*/
|
||||
removeInboundPattern(index: number) {
|
||||
const notifyServiceInboundPatternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
|
||||
notifyServiceInboundPatternsArray.removeAt(index);
|
||||
selectInboundPattern(patternValue: string, index: number): void {
|
||||
const patternArray = (this.formModel.get('notifyServiceInboundPatterns') as FormArray);
|
||||
patternArray.controls[index].patchValue({pattern: patternValue});
|
||||
patternArray.controls[index].patchValue({patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternValue + '.label')});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new outbound pattern form group to the notifyServiceOutboundPatterns form array.
|
||||
* Selects an inbound item filter by updating its value based on the provided filter value and index
|
||||
* @param filterValue - The selected filter value
|
||||
* @param index - The index of the inbound pattern in the array
|
||||
*/
|
||||
addOutboundPattern() {
|
||||
const notifyServiceOutboundPatternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray;
|
||||
notifyServiceOutboundPatternsArray.push(this.createOutboundPatternFormGroup());
|
||||
selectInboundItemFilter(filterValue: string, index: number): void {
|
||||
const filterArray = (this.formModel.get('notifyServiceInboundPatterns') as FormArray);
|
||||
filterArray.controls[index].patchValue({constraint: filterValue});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the outbound pattern form group at the specified index from the notifyServiceOutboundPatterns form array.
|
||||
*
|
||||
* @param {number} index - The index of the outbound pattern form group to remove.
|
||||
*/
|
||||
removeOutboundPattern(index: number) {
|
||||
const notifyServiceOutboundPatternsArray = this.formModel.get('notifyServiceOutboundPatterns') as FormArray;
|
||||
notifyServiceOutboundPatternsArray.removeAt(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the value of the 'automatic' control at the specified index in the notifyServiceInboundPatterns form array.
|
||||
*
|
||||
* @param {number} i - The index of the 'automatic' control to toggle.
|
||||
* @memberof LdnServiceFormComponent
|
||||
* Toggles the automatic property of an inbound pattern at the specified index
|
||||
* @param i - The index of the inbound pattern in the array
|
||||
*/
|
||||
toggleAutomatic(i: number) {
|
||||
const automaticControl = this.formModel.get(`notifyServiceInboundPatterns.${i}.automatic`);
|
||||
@@ -285,88 +331,232 @@ export class LdnServiceFormComponent implements OnInit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects an outbound pattern for a specific index in the notifyServiceOutboundPatterns form array.
|
||||
*
|
||||
* @param {string} patternValue - The selected pattern value.
|
||||
* @param {number} index - The index of the outbound pattern in the form array.
|
||||
* Toggles the enabled status of the LDN service by sending a patch request
|
||||
*/
|
||||
selectOutboundPattern(patternValue: string, index: number): void {
|
||||
const patternArray = (this.formModel.get('notifyServiceOutboundPatterns') as FormArray);
|
||||
patternArray.controls[index].patchValue({pattern: patternValue});
|
||||
patternArray.controls[index].patchValue({patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternValue + '.label')});
|
||||
toggleEnabled() {
|
||||
const newStatus = !this.formModel.get('enabled').value;
|
||||
|
||||
const patchOperation: Operation = {
|
||||
op: 'replace',
|
||||
path: '/enabled',
|
||||
value: newStatus,
|
||||
};
|
||||
|
||||
this.ldnServicesService.patch(this.service, [patchOperation]).pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe(
|
||||
() => {
|
||||
|
||||
this.formModel.get('enabled').setValue(newStatus);
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects an inbound pattern for a specific index in the form array.
|
||||
*
|
||||
* @param {string} patternValue - The selected pattern value.
|
||||
* @param {number} index - The index of the inbound pattern in the form array.
|
||||
* Closes the modal
|
||||
*/
|
||||
selectInboundPattern(patternValue: string, index: number): void {
|
||||
const patternArray = (this.formModel.get('notifyServiceInboundPatterns') as FormArray);
|
||||
patternArray.controls[index].patchValue({pattern: patternValue});
|
||||
patternArray.controls[index].patchValue({patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternValue + '.label')});
|
||||
|
||||
closeModal() {
|
||||
this.modalRef.close();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects an inbound item filter for a specific index in the form array.
|
||||
*
|
||||
* @param {string} filterValue - The selected item filter value.
|
||||
* @param {number} index - The index of the inbound item filter in the form array.
|
||||
* Opens a confirmation modal with the specified content
|
||||
* @param content - The content to be displayed in the modal
|
||||
*/
|
||||
selectInboundItemFilter(filterValue: string, index: number): void {
|
||||
const filterArray = (this.formModel.get('notifyServiceInboundPatterns') as FormArray);
|
||||
filterArray.controls[index].patchValue({constraint: filterValue});
|
||||
openConfirmModal(content) {
|
||||
this.modalRef = this.modalService.open(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects an outbound item filter for a specific index in the form array.
|
||||
*
|
||||
* @param {string} filterValue - The selected item filter value.
|
||||
* @param {number} index - The index of the outbound item filter in the form array.
|
||||
* Opens a reset form modal with the specified content
|
||||
* @param content - The content to be displayed in the modal
|
||||
*/
|
||||
selectOutboundItemFilter(filterValue: string, index: number) {
|
||||
const filterArray = (this.formModel.get('notifyServiceOutboundPatterns') as FormArray);
|
||||
filterArray.controls[index].patchValue({constraint: filterValue});
|
||||
openResetFormModal(content) {
|
||||
this.modalRef = this.modalService.open(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the user back to the LDN services list.
|
||||
* Patches the LDN service by retrieving and sending patch operations geenrated in generatePatchOperations()
|
||||
*/
|
||||
patchService() {
|
||||
this.deleteMarkedInboundPatterns();
|
||||
|
||||
const patchOperations = this.generatePatchOperations();
|
||||
this.formModel.markAllAsTouched();
|
||||
// If the form is invalid, close the modal and return
|
||||
if (this.formModel.invalid) {
|
||||
this.closeModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const notifyServiceInboundPatterns = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
|
||||
const deletedInboundPatternsLength = this.deletedInboundPatterns.length;
|
||||
// If no inbound patterns are specified, close the modal and return
|
||||
// notify the user that no patterns are specified
|
||||
if (notifyServiceInboundPatterns.length === deletedInboundPatternsLength) {
|
||||
this.notificationService.warning(this.translateService.get('ldn-service-notification.created.warning.title'));
|
||||
this.deletedInboundPatterns = [];
|
||||
this.closeModal();
|
||||
return;
|
||||
}
|
||||
|
||||
this.ldnServicesService.patch(this.service, patchOperations).pipe(
|
||||
getFirstCompletedRemoteData()
|
||||
).subscribe(
|
||||
(rd: RemoteData<LdnService>) => {
|
||||
if (rd.hasSucceeded) {
|
||||
this.closeModal();
|
||||
this.sendBack();
|
||||
this.notificationService.success(this.translateService.get('admin.registries.services-formats.modify.success.head'),
|
||||
this.translateService.get('admin.registries.services-formats.modify.success.content'));
|
||||
} else {
|
||||
this.notificationService.error(this.translateService.get('admin.registries.services-formats.modify.failure.head'),
|
||||
this.translateService.get('admin.registries.services-formats.modify.failure.content'));
|
||||
this.closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the form and navigates back to the LDN services page
|
||||
*/
|
||||
resetFormAndLeave() {
|
||||
this.sendBack();
|
||||
this.closeModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the specified inbound pattern for deletion
|
||||
* @param index - The index of the inbound pattern in the array
|
||||
*/
|
||||
markForInboundPatternDeletion(index: number) {
|
||||
if (!this.markedForDeletionInboundPattern.includes(index)) {
|
||||
this.markedForDeletionInboundPattern.push(index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmarks the specified inbound pattern for deletion
|
||||
* @param index - The index of the inbound pattern in the array
|
||||
*/
|
||||
unmarkForInboundPatternDeletion(index: number) {
|
||||
const i = this.markedForDeletionInboundPattern.indexOf(index);
|
||||
if (i !== -1) {
|
||||
this.markedForDeletionInboundPattern.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes marked inbound patterns from the form model
|
||||
*/
|
||||
deleteMarkedInboundPatterns() {
|
||||
this.markedForDeletionInboundPattern.sort((a, b) => b - a);
|
||||
const patternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
|
||||
|
||||
for (const index of this.markedForDeletionInboundPattern) {
|
||||
if (index >= 0 && index < patternsArray.length) {
|
||||
const patternGroup = patternsArray.at(index) as FormGroup;
|
||||
const patternValue = patternGroup.value;
|
||||
if (patternValue.isNew) {
|
||||
patternsArray.removeAt(index);
|
||||
} else {
|
||||
this.deletedInboundPatterns.push(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.markedForDeletionInboundPattern = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a replace operation and adds it to the patch operations if the form control is dirty
|
||||
* @param patchOperations - The array to store patch operations
|
||||
* @param formControlName - The name of the form control
|
||||
* @param path - The JSON Patch path for the operation
|
||||
*/
|
||||
private createReplaceOperation(patchOperations: any[], formControlName: string, path: string): void {
|
||||
if (this.formModel.get(formControlName).dirty) {
|
||||
patchOperations.push({
|
||||
op: 'replace',
|
||||
path,
|
||||
value: this.formModel.get(formControlName).value.toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles patterns in the form array, checking if an add or replace operations is required
|
||||
* @param patchOperations - The array to store patch operations
|
||||
* @param formArrayName - The name of the form array
|
||||
*/
|
||||
private handlePatterns(patchOperations: any[], formArrayName: string): void {
|
||||
const patternsArray = this.formModel.get(formArrayName) as FormArray;
|
||||
|
||||
for (let i = 0; i < patternsArray.length; i++) {
|
||||
const patternGroup = patternsArray.at(i) as FormGroup;
|
||||
|
||||
const patternValue = patternGroup.value;
|
||||
if (patternGroup.touched && patternGroup.valid) {
|
||||
delete patternValue?.patternLabel;
|
||||
if (patternValue.isNew) {
|
||||
delete patternValue.isNew;
|
||||
const addOperation = {
|
||||
op: 'add',
|
||||
path: `${formArrayName}/-`,
|
||||
value: patternValue,
|
||||
};
|
||||
patchOperations.push(addOperation);
|
||||
} else {
|
||||
const replaceOperation = {
|
||||
op: 'replace',
|
||||
path: `${formArrayName}[${i}]`,
|
||||
value: patternValue,
|
||||
};
|
||||
patchOperations.push(replaceOperation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates back to the LDN services page
|
||||
*/
|
||||
private sendBack() {
|
||||
this.router.navigateByUrl('admin/ldn/services');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a form group for an outbound pattern in the notifyServiceOutboundPatterns form array.
|
||||
*
|
||||
* @private
|
||||
* @returns {FormGroup} - The created form group.
|
||||
* Creates a form group for inbound patterns
|
||||
* @returns The form group for inbound patterns
|
||||
*/
|
||||
private createOutboundPatternFormGroup(): FormGroup {
|
||||
return this.formBuilder.group({
|
||||
pattern: [''],
|
||||
constraint: [''],
|
||||
private createInboundPatternFormGroup(): FormGroup {
|
||||
const inBoundFormGroup = {
|
||||
pattern: '',
|
||||
patternLabel: this.translateService.instant(this.selectPatternDefaultLabeli18Key),
|
||||
});
|
||||
constraint: '',
|
||||
automatic: false,
|
||||
isNew: true
|
||||
};
|
||||
|
||||
if (this.isNewService) {
|
||||
delete inBoundFormGroup.isNew;
|
||||
}
|
||||
|
||||
return this.formBuilder.group(inBoundFormGroup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a form group for an inbound pattern in the notifyServiceInboundPatterns form array.
|
||||
*
|
||||
* @private
|
||||
* @returns {FormGroup} - The created form group.
|
||||
* Initializes an existing form group for inbound patterns
|
||||
* @returns The initialized form group for inbound patterns
|
||||
*/
|
||||
private createInboundPatternFormGroup(): FormGroup {
|
||||
private initializeInboundPatternFormGroup(): FormGroup {
|
||||
return this.formBuilder.group({
|
||||
pattern: [''],
|
||||
constraint: [''],
|
||||
automatic: false,
|
||||
patternLabel: this.translateService.instant(this.selectPatternDefaultLabeli18Key),
|
||||
pattern: '',
|
||||
patternLabel: '',
|
||||
constraint: '',
|
||||
automatic: '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -1 +0,0 @@
|
||||
<ds-ldn-service-form></ds-ldn-service-form>
|
@@ -1,25 +0,0 @@
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {LdnServiceNewComponent} from './ldn-service-new.component';
|
||||
|
||||
describe('LdnServiceNewComponent', () => {
|
||||
let component: LdnServiceNewComponent;
|
||||
let fixture: ComponentFixture<LdnServiceNewComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [LdnServiceNewComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LdnServiceNewComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -1,9 +0,0 @@
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-ldn-service-new',
|
||||
templateUrl: './ldn-service-new.component.html',
|
||||
styleUrls: ['./ldn-service-new.component.scss']
|
||||
})
|
||||
export class LdnServiceNewComponent {
|
||||
}
|
@@ -26,13 +26,6 @@ export const mockLdnService: LdnService = {
|
||||
automatic: 'true',
|
||||
},
|
||||
],
|
||||
notifyServiceOutboundPatterns: [
|
||||
{
|
||||
pattern: 'patternC',
|
||||
constraint: 'itemFilterC',
|
||||
automatic: 'true',
|
||||
},
|
||||
],
|
||||
type: LDN_SERVICE,
|
||||
_links: {
|
||||
self: {
|
||||
@@ -68,13 +61,6 @@ export const mockLdnServices: LdnService[] = [{
|
||||
automatic: 'true',
|
||||
},
|
||||
],
|
||||
notifyServiceOutboundPatterns: [
|
||||
{
|
||||
pattern: 'patternC',
|
||||
constraint: 'itemFilterC',
|
||||
automatic: 'true',
|
||||
},
|
||||
],
|
||||
type: LDN_SERVICE,
|
||||
_links: {
|
||||
self: {
|
||||
@@ -105,13 +91,6 @@ export const mockLdnServices: LdnService[] = [{
|
||||
automatic: 'true',
|
||||
},
|
||||
],
|
||||
notifyServiceOutboundPatterns: [
|
||||
{
|
||||
pattern: 'patternC',
|
||||
constraint: 'itemFilterC',
|
||||
automatic: 'true',
|
||||
},
|
||||
],
|
||||
type: LDN_SERVICE,
|
||||
_links: {
|
||||
self: {
|
||||
|
@@ -44,9 +44,6 @@ export class LdnService extends CacheableObject {
|
||||
@autoserialize
|
||||
notifyServiceInboundPatterns?: NotifyServicePattern[];
|
||||
|
||||
@autoserialize
|
||||
notifyServiceOutboundPatterns?: NotifyServicePattern[];
|
||||
|
||||
@deserialize
|
||||
_links: {
|
||||
self: {
|
||||
|
@@ -14,7 +14,11 @@ import { AdminQualityAssuranceTopicsPageResolver } from './admin-quality-assuran
|
||||
import { AdminQualityAssuranceEventsPageResolver } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.resolver';
|
||||
import { AdminQualityAssuranceSourcePageComponent } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component';
|
||||
import { AdminQualityAssuranceSourcePageResolver } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page-resolver.service';
|
||||
import { SourceDataResolver } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-data.reslover';
|
||||
import { QualityAssuranceBreadcrumbResolver } from '../../core/breadcrumbs/quality-assurance-breadcrumb.resolver';
|
||||
import { QualityAssuranceBreadcrumbService } from '../../core/breadcrumbs/quality-assurance-breadcrumb.service';
|
||||
import {
|
||||
SourceDataResolver
|
||||
} from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-data.resolver';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -40,7 +44,7 @@ import { SourceDataResolver } from './admin-quality-assurance-source-page-compon
|
||||
component: AdminQualityAssuranceTopicsPageComponent,
|
||||
pathMatch: 'full',
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver,
|
||||
breadcrumb: QualityAssuranceBreadcrumbResolver,
|
||||
openaireQualityAssuranceTopicsParams: AdminQualityAssuranceTopicsPageResolver
|
||||
},
|
||||
data: {
|
||||
@@ -86,7 +90,7 @@ import { SourceDataResolver } from './admin-quality-assurance-source-page-compon
|
||||
component: AdminQualityAssuranceEventsPageComponent,
|
||||
pathMatch: 'full',
|
||||
resolve: {
|
||||
breadcrumb: I18nBreadcrumbResolver,
|
||||
breadcrumb: QualityAssuranceBreadcrumbResolver,
|
||||
openaireQualityAssuranceEventsParams: AdminQualityAssuranceEventsPageResolver
|
||||
},
|
||||
data: {
|
||||
@@ -105,6 +109,9 @@ import { SourceDataResolver } from './admin-quality-assurance-source-page-compon
|
||||
AdminQualityAssuranceSourcePageResolver,
|
||||
AdminQualityAssuranceTopicsPageResolver,
|
||||
AdminQualityAssuranceEventsPageResolver,
|
||||
AdminQualityAssuranceSourcePageResolver,
|
||||
QualityAssuranceBreadcrumbResolver,
|
||||
QualityAssuranceBreadcrumbService
|
||||
]
|
||||
})
|
||||
/**
|
||||
|
@@ -7,7 +7,7 @@ import { AdminNotificationsSuggestionTargetsPageComponent } from './admin-notifi
|
||||
import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component';
|
||||
import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.component';
|
||||
import { AdminQualityAssuranceSourcePageComponent } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component';
|
||||
import {SuggestionNotificationsModule} from '../../suggestion-notifications/suggestion-notifications.module';
|
||||
import {NotificationsModule} from '../../notifications/notifications.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -15,7 +15,7 @@ import {SuggestionNotificationsModule} from '../../suggestion-notifications/sugg
|
||||
SharedModule,
|
||||
CoreModule.forRoot(),
|
||||
AdminNotificationsRoutingModule,
|
||||
SuggestionNotificationsModule
|
||||
NotificationsModule
|
||||
],
|
||||
declarations: [
|
||||
AdminNotificationsSuggestionTargetsPageComponent,
|
||||
|
@@ -3,13 +3,15 @@ import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot, Router } from '@a
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { QualityAssuranceSourceObject } from '../../../core/suggestion-notifications/qa/models/quality-assurance-source.model';
|
||||
import { QualityAssuranceSourceService } from '../../../suggestion-notifications/qa/source/quality-assurance-source.service';
|
||||
import { QualityAssuranceSourceObject } from '../../../core/notifications/qa/models/quality-assurance-source.model';
|
||||
import { QualityAssuranceSourceService } from '../../../notifications/qa/source/quality-assurance-source.service';
|
||||
import {environment} from '../../../../environments/environment';
|
||||
/**
|
||||
* This class represents a resolver that retrieve the route data before the route is activated.
|
||||
*/
|
||||
@Injectable()
|
||||
export class SourceDataResolver implements Resolve<Observable<QualityAssuranceSourceObject[]>> {
|
||||
private pageSize = environment.qualityAssuranceConfig.pageSize;
|
||||
/**
|
||||
* Initialize the effect class variables.
|
||||
* @param {QualityAssuranceSourceService} qualityAssuranceSourceService
|
||||
@@ -25,7 +27,7 @@ export class SourceDataResolver implements Resolve<Observable<QualityAssuranceSo
|
||||
* @returns Observable<QualityAssuranceSourceObject[]>
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<QualityAssuranceSourceObject[]> {
|
||||
return this.qualityAssuranceSourceService.getSources(5,0).pipe(
|
||||
return this.qualityAssuranceSourceService.getSources(this.pageSize, 0).pipe(
|
||||
map((sources: PaginatedList<QualityAssuranceSourceObject>) => {
|
||||
if (sources.page.length === 1) {
|
||||
this.router.navigate([this.getResolvedUrl(route) + '/' + sources.page[0].id]);
|
@@ -1,11 +1,11 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4">
|
||||
<h2 id="sub-header"
|
||||
class="border-bottom mb-2">{{ 'admin.registries.bitstream-formats.create.new' | translate }}</h2>
|
||||
<h1 id="sub-header"
|
||||
class="border-bottom pb-2">{{ 'admin.registries.bitstream-formats.create.new' | translate }}</h1>
|
||||
|
||||
<ds-bitstream-format-form (updatedFormat)="createBitstreamFormat($event)"></ds-bitstream-format-form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div class="bitstream-formats row">
|
||||
<div class="col-12">
|
||||
|
||||
<h2 id="header" class="border-bottom pb-2 ">{{'admin.registries.bitstream-formats.head' | translate}}</h2>
|
||||
<h1 id="header" class="border-bottom pb-2">{{'admin.registries.bitstream-formats.head' | translate}}</h1>
|
||||
|
||||
<p id="description">{{'admin.registries.bitstream-formats.description' | translate}}</p>
|
||||
<p id="create-new" class="mb-2"><a [routerLink]="'add'" class="btn btn-success">{{'admin.registries.bitstream-formats.create.new' | translate}}</a></p>
|
||||
@@ -19,7 +19,7 @@
|
||||
<table id="formats" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
<th scope="col" [attr.aria-label]="'admin.registries.bitstream-formats.select' | translate"></th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.id' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.name' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.mimetype' | translate}}</th>
|
||||
@@ -31,6 +31,7 @@
|
||||
<td>
|
||||
<label class="mb-0">
|
||||
<input type="checkbox"
|
||||
[attr.aria-label]="'admin.registries.bitstream-formats.select' | translate"
|
||||
[checked]="isSelected(bitstreamFormat) | async"
|
||||
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
||||
>
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4">
|
||||
<h2 id="sub-header"
|
||||
class="border-bottom mb-2">{{'admin.registries.bitstream-formats.edit.head' | translate:{format: (bitstreamFormatRD$ | async)?.payload.shortDescription} }}</h2>
|
||||
<h1 id="sub-header"
|
||||
class="border-bottom pb-2">{{'admin.registries.bitstream-formats.edit.head' | translate:{format: (bitstreamFormatRD$ | async)?.payload.shortDescription} }}</h1>
|
||||
|
||||
<ds-bitstream-format-form [bitstreamFormat]="(bitstreamFormatRD$ | async)?.payload" (updatedFormat)="updateFormat($event)"></ds-bitstream-format-form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -8,9 +8,9 @@ import {
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { switchMap, take, tap } from 'rxjs/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { Observable, combineLatest } from 'rxjs';
|
||||
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
||||
|
||||
@Component({
|
||||
@@ -147,30 +147,48 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
* Emit the updated/created schema using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit(): void {
|
||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
|
||||
(schema: MetadataSchema) => {
|
||||
const values = {
|
||||
prefix: this.name.value,
|
||||
namespace: this.namespace.value
|
||||
};
|
||||
if (schema == null) {
|
||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), values)).subscribe((newSchema) => {
|
||||
this.submitForm.emit(newSchema);
|
||||
this.registryService
|
||||
.getActiveMetadataSchema()
|
||||
.pipe(
|
||||
take(1),
|
||||
switchMap((schema: MetadataSchema) => {
|
||||
const metadataValues = {
|
||||
prefix: this.name.value,
|
||||
namespace: this.namespace.value,
|
||||
};
|
||||
|
||||
let createOrUpdate$: Observable<MetadataSchema>;
|
||||
|
||||
if (schema == null) {
|
||||
createOrUpdate$ =
|
||||
this.registryService.createOrUpdateMetadataSchema(
|
||||
Object.assign(new MetadataSchema(), metadataValues)
|
||||
);
|
||||
} else {
|
||||
const updatedSchema = Object.assign(
|
||||
new MetadataSchema(),
|
||||
schema,
|
||||
{
|
||||
namespace: metadataValues.namespace,
|
||||
}
|
||||
);
|
||||
createOrUpdate$ =
|
||||
this.registryService.createOrUpdateMetadataSchema(
|
||||
updatedSchema
|
||||
);
|
||||
}
|
||||
|
||||
return createOrUpdate$;
|
||||
}),
|
||||
tap(() => {
|
||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||
})
|
||||
)
|
||||
.subscribe((updatedOrCreatedSchema: MetadataSchema) => {
|
||||
this.submitForm.emit(updatedOrCreatedSchema);
|
||||
this.clearFields();
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
});
|
||||
} else {
|
||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
|
||||
id: schema.id,
|
||||
prefix: schema.prefix,
|
||||
namespace: values.namespace,
|
||||
})).subscribe((updatedSchema: MetadataSchema) => {
|
||||
this.submitForm.emit(updatedSchema);
|
||||
});
|
||||
}
|
||||
this.clearFields();
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -3,7 +3,8 @@ import {
|
||||
DynamicFormControlModel,
|
||||
DynamicFormGroupModel,
|
||||
DynamicFormLayout,
|
||||
DynamicInputModel
|
||||
DynamicInputModel,
|
||||
DynamicTextAreaModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
@@ -51,7 +52,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* A dynamic input model for the scopeNote field
|
||||
*/
|
||||
scopeNote: DynamicInputModel;
|
||||
scopeNote: DynamicTextAreaModel;
|
||||
|
||||
/**
|
||||
* A list of all dynamic input models
|
||||
@@ -132,11 +133,12 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
||||
maxLength: 'error.validation.metadata.qualifier.max-length',
|
||||
},
|
||||
});
|
||||
this.scopeNote = new DynamicInputModel({
|
||||
this.scopeNote = new DynamicTextAreaModel({
|
||||
id: 'scopeNote',
|
||||
label: scopenote,
|
||||
name: 'scopeNote',
|
||||
required: false,
|
||||
rows: 5,
|
||||
});
|
||||
this.formModel = [
|
||||
new DynamicFormGroupModel(
|
||||
|
@@ -41,7 +41,7 @@
|
||||
</label>
|
||||
</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{field.id}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier" class="mb-0">.</label>{{field.qualifier}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}{{field.qualifier ? '.' + field.qualifier : ''}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { RegistryService } from '../../../core/registry/registry.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest as observableCombineLatest,
|
||||
@@ -32,7 +32,7 @@ import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
* A component used for managing all existing metadata fields within the current metadata schema.
|
||||
* The admin can create, edit or delete metadata fields here.
|
||||
*/
|
||||
export class MetadataSchemaComponent implements OnInit {
|
||||
export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* The metadata schema
|
||||
*/
|
||||
@@ -60,7 +60,6 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
constructor(private registryService: RegistryService,
|
||||
private route: ActivatedRoute,
|
||||
private notificationsService: NotificationsService,
|
||||
private router: Router,
|
||||
private paginationService: PaginationService,
|
||||
private translateService: TranslateService) {
|
||||
|
||||
@@ -86,7 +85,7 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
*/
|
||||
private updateFields() {
|
||||
this.metadataFields$ = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||
switchMap((currentPagination) => combineLatest(this.metadataSchema$, this.needsUpdate$, observableOf(currentPagination))),
|
||||
switchMap((currentPagination) => combineLatest([this.metadataSchema$, this.needsUpdate$, observableOf(currentPagination)])),
|
||||
switchMap(([schema, update, currentPagination]: [MetadataSchema, boolean, PaginationComponentOptions]) => {
|
||||
if (update) {
|
||||
this.needsUpdate$.next(false);
|
||||
@@ -193,10 +192,10 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
showNotification(success: boolean, amount: number) {
|
||||
const prefix = 'admin.registries.schema.notification';
|
||||
const suffix = success ? 'success' : 'failure';
|
||||
const messages = observableCombineLatest(
|
||||
const messages = observableCombineLatest([
|
||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
||||
this.translateService.get(`${prefix}.field.deleted.${suffix}`, { amount: amount })
|
||||
);
|
||||
]);
|
||||
messages.subscribe(([head, content]) => {
|
||||
if (success) {
|
||||
this.notificationsService.success(head, content);
|
||||
@@ -207,6 +206,7 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
this.registryService.deselectAllMetadataField();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -12,8 +12,7 @@ import { Router } from '@angular/router';
|
||||
* Represents a non-expandable section in the admin sidebar
|
||||
*/
|
||||
@Component({
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
selector: 'li[ds-admin-sidebar-section]',
|
||||
selector: 'ds-admin-sidebar-section',
|
||||
templateUrl: './admin-sidebar-section.component.html',
|
||||
styleUrls: ['./admin-sidebar-section.component.scss'],
|
||||
|
||||
|
@@ -26,30 +26,29 @@
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<ng-container *ngFor="let section of (sections | async)">
|
||||
<li *ngFor="let section of (sections | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-nav">
|
||||
<div class="sidebar-section" id="sidebar-collapse-toggle">
|
||||
<a class="nav-item nav-link sidebar-section d-flex flex-row flex-nowrap"
|
||||
href="javascript:void(0);"
|
||||
<button class="nav-item nav-link sidebar-section d-flex flex-row flex-nowrap border-0" type="button"
|
||||
(click)="toggle($event)"
|
||||
(keyup.space)="toggle($event)"
|
||||
>
|
||||
<div class="shortcut-icon">
|
||||
<span class="shortcut-icon">
|
||||
<i *ngIf="(menuCollapsed | async)" class="fas fa-fw fa-angle-double-right"
|
||||
[title]="'menu.section.icon.pin' | translate"></i>
|
||||
<i *ngIf="!(menuCollapsed | async)" class="fas fa-fw fa-angle-double-left"
|
||||
[title]="'menu.section.icon.unpin' | translate"></i>
|
||||
</div>
|
||||
<div class="sidebar-collapsible">
|
||||
</span>
|
||||
<span class="sidebar-collapsible text-left">
|
||||
<span *ngIf="menuCollapsed | async" class="section-header-text">{{'menu.section.pin' | translate }}</span>
|
||||
<span *ngIf="!(menuCollapsed | async)" class="section-header-text">{{'menu.section.unpin' | translate }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
@@ -143,7 +143,7 @@ describe('AdminSidebarComponent', () => {
|
||||
describe('when the collapse link is clicked', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(menuService, 'toggleMenu');
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle > a'));
|
||||
const sidebarToggler = fixture.debugElement.query(By.css('#sidebar-collapse-toggle > button'));
|
||||
sidebarToggler.triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
|
@@ -15,8 +15,7 @@ import { Router } from '@angular/router';
|
||||
* Represents a expandable section in the sidebar
|
||||
*/
|
||||
@Component({
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
selector: 'li[ds-expandable-admin-sidebar-section]',
|
||||
selector: 'ds-expandable-admin-sidebar-section',
|
||||
templateUrl: './expandable-admin-sidebar-section.component.html',
|
||||
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
|
||||
animations: [rotate, slide, bgColor]
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="container">
|
||||
<h3>{{'bitstream.download.page' | translate:{ bitstream: dsoNameService.getName((bitstream$ | async)) } }}</h3>
|
||||
<h1 class="h2">{{'bitstream.download.page' | translate:{ bitstream: dsoNameService.getName((bitstream$ | async)) } }}</h1>
|
||||
<div class="pt-3">
|
||||
<button (click)="back()" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> {{'bitstream.download.page.back' | translate}}
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h3>{{dsoNameService.getName(bitstreamRD?.payload)}} <span class="text-muted">({{bitstreamRD?.payload?.sizeBytes | dsFileSize}})</span></h3>
|
||||
<h1 class="h2">{{dsoNameService.getName(bitstreamRD?.payload)}} <span class="text-muted">({{bitstreamRD?.payload?.sizeBytes | dsFileSize}})</span></h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<ng-container *ngVar="(breadcrumbs$ | async) as breadcrumbs">
|
||||
<nav *ngIf="(showBreadcrumbs$ | async)" aria-label="breadcrumb" class="nav-breadcrumb">
|
||||
<ol class="container breadcrumb">
|
||||
<ol class="container breadcrumb my-0">
|
||||
<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;">
|
||||
|
@@ -4,9 +4,8 @@
|
||||
|
||||
.breadcrumb {
|
||||
border-radius: 0;
|
||||
margin-top: calc(-1 * var(--ds-content-spacing));
|
||||
padding-bottom: var(--ds-content-spacing / 3);
|
||||
padding-top: var(--ds-content-spacing / 3);
|
||||
padding-bottom: calc(var(--ds-content-spacing) / 2);
|
||||
padding-top: calc(var(--ds-content-spacing) / 2);
|
||||
background-color: var(--ds-breadcrumb-bg);
|
||||
}
|
||||
|
||||
|
@@ -89,11 +89,11 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||
const lastItemRD = this.browseService.getFirstItemFor(definition, scope, SortDirection.DESC);
|
||||
this.subs.push(
|
||||
observableCombineLatest([firstItemRD, lastItemRD]).subscribe(([firstItem, lastItem]) => {
|
||||
let lowerLimit = this.getLimit(firstItem, metadataKeys, this.appConfig.browseBy.defaultLowerLimit);
|
||||
let upperLimit = this.getLimit(lastItem, metadataKeys, new Date().getUTCFullYear());
|
||||
const options = [];
|
||||
const oneYearBreak = Math.floor((upperLimit - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
|
||||
const fiveYearBreak = Math.floor((upperLimit - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
|
||||
let lowerLimit: number = this.getLimit(firstItem, metadataKeys, this.appConfig.browseBy.defaultLowerLimit);
|
||||
let upperLimit: number = this.getLimit(lastItem, metadataKeys, new Date().getUTCFullYear());
|
||||
const options: number[] = [];
|
||||
const oneYearBreak: number = Math.floor((upperLimit - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
|
||||
const fiveYearBreak: number = Math.floor((upperLimit - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
|
||||
if (lowerLimit <= fiveYearBreak) {
|
||||
lowerLimit -= 10;
|
||||
} else if (lowerLimit <= oneYearBreak) {
|
||||
@@ -101,7 +101,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
||||
} else {
|
||||
lowerLimit -= 1;
|
||||
}
|
||||
let i = upperLimit;
|
||||
let i: number = upperLimit;
|
||||
while (i > lowerLimit) {
|
||||
options.push(i);
|
||||
if (i <= fiveYearBreak) {
|
||||
|
@@ -32,7 +32,7 @@
|
||||
|
||||
<section class="comcol-page-browse-section">
|
||||
<div class="browse-by-metadata w-100">
|
||||
<ds-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100"
|
||||
<ds-themed-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100"
|
||||
title="{{'browse.title' | translate:
|
||||
{
|
||||
collection: dsoNameService.getName((parent$ | async)?.payload),
|
||||
@@ -48,7 +48,7 @@
|
||||
[startsWithOptions]="startsWithOptions"
|
||||
(prev)="goPrev()"
|
||||
(next)="goNext()">
|
||||
</ds-browse-by>
|
||||
</ds-themed-browse-by>
|
||||
<ds-themed-loading *ngIf="!startsWithOptions" message="{{'loading.browse-by-page' | translate}}"></ds-themed-loading>
|
||||
</div>
|
||||
</section>
|
||||
|
@@ -161,7 +161,11 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
||||
this.value = '';
|
||||
}
|
||||
|
||||
if (typeof params.startsWith === 'string'){
|
||||
if (params.startsWith === undefined || params.startsWith === '') {
|
||||
this.startsWith = undefined;
|
||||
}
|
||||
|
||||
if (typeof params.startsWith === 'string'){
|
||||
this.startsWith = params.startsWith.trim();
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<div class="container">
|
||||
<h1>{{ ('browse.taxonomy_' + vocabularyName + '.title') | translate }}</h1>
|
||||
<div class="mb-3">
|
||||
<ds-vocabulary-treeview [vocabularyOptions]=vocabularyOptions
|
||||
[multiSelect]="true"
|
||||
|
@@ -14,6 +14,7 @@ import { ThemedBrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page/t
|
||||
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
|
||||
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
|
||||
import { FormModule } from '../shared/form/form.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put only entry components that use custom decorator
|
||||
@@ -35,6 +36,7 @@ const ENTRY_COMPONENTS = [
|
||||
ComcolModule,
|
||||
DsoPageModule,
|
||||
FormModule,
|
||||
SharedModule,
|
||||
],
|
||||
declarations: [
|
||||
BrowseBySwitcherComponent,
|
||||
|
@@ -98,9 +98,8 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
|
||||
// retrieve all entity types to populate the dropdowns selection
|
||||
entities$.subscribe((entityTypes: ItemType[]) => {
|
||||
|
||||
entityTypes
|
||||
.filter((type: ItemType) => type.label !== NONE_ENTITY_TYPE)
|
||||
.forEach((type: ItemType, index: number) => {
|
||||
entityTypes = entityTypes.filter((type: ItemType) => type.label !== NONE_ENTITY_TYPE);
|
||||
entityTypes.forEach((type: ItemType, index: number) => {
|
||||
this.entityTypeSelection.add({
|
||||
disabled: false,
|
||||
label: type.label,
|
||||
@@ -112,7 +111,7 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
|
||||
}
|
||||
});
|
||||
|
||||
this.formModel = [...collectionFormModels, this.entityTypeSelection];
|
||||
this.formModel = entityTypes.length === 0 ? collectionFormModels : [...collectionFormModels, this.entityTypeSelection];
|
||||
|
||||
super.ngOnInit();
|
||||
this.chd.detectChanges();
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<p>{{'collection.edit.item-mapper.description' | translate}}</p>
|
||||
|
||||
<ul ngbNav (navChange)="tabChange($event)" [destroyOnHide]="true" #tabs="ngbNav" class="nav-tabs">
|
||||
<li [ngbNavItem]="'browseTab'">
|
||||
<li [ngbNavItem]="'browseTab'" role="presentation">
|
||||
<a ngbNavLink>{{'collection.edit.item-mapper.tabs.browse' | translate}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="mt-2">
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="'mapTab'">
|
||||
<li [ngbNavItem]="'mapTab'" role="presentation">
|
||||
<a ngbNavLink>{{'collection.edit.item-mapper.tabs.map' | translate}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row mt-2">
|
||||
|
@@ -34,9 +34,6 @@
|
||||
</ds-comcol-page-content>
|
||||
</header>
|
||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||
<div class="pl-2 space-children-mr">
|
||||
<ds-dso-page-subscription-button [dso]="collection"></ds-dso-page-subscription-button>
|
||||
</div>
|
||||
</div>
|
||||
<section class="comcol-page-browse-section">
|
||||
<!-- Browse-By Links -->
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div class="row">
|
||||
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'collection.delete.head' | translate}}</h2>
|
||||
<h1 id="header" class="border-bottom pb-2">{{ 'collection.delete.head' | translate}}</h1>
|
||||
<p class="pb-2">{{ 'collection.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}</p>
|
||||
<div class="form-group row">
|
||||
<div class="col text-right space-children-mr">
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="container">
|
||||
<h3>{{'collection.curate.header' |translate:{collection: (collectionName$ |async)} }}</h3>
|
||||
<h2>{{'collection.curate.header' |translate:{collection: (collectionName$ |async)} }}</h2>
|
||||
<ds-curation-form
|
||||
[dsoHandle]="(dsoRD$|async)?.payload.handle"
|
||||
></ds-curation-form>
|
||||
|
@@ -18,7 +18,7 @@
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h4>{{ 'collection.edit.tabs.source.head' | translate }}</h4>
|
||||
<h2>{{ 'collection.edit.tabs.source.head' | translate }}</h2>
|
||||
<div *ngIf="contentSource" class="form-check mb-4">
|
||||
<input type="checkbox" class="form-check-input" id="externalSourceCheck"
|
||||
[checked]="(contentSource?.harvestType !== harvestTypeNone)" (change)="changeExternalSource()">
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div class="row">
|
||||
<div class="col-12" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<ng-container *ngIf="itemRD?.hasSucceeded">
|
||||
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: dsoNameService.getName(collection) } }}</h2>
|
||||
<h1 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: dsoNameService.getName(collection) } }}</h1>
|
||||
<ds-themed-dso-edit-metadata [updateDataService]="itemTemplateService" [dso]="itemRD?.payload"></ds-themed-dso-edit-metadata>
|
||||
<button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
|
||||
</ng-container>
|
||||
|
@@ -8,7 +8,7 @@ import { ItemTemplateDataService } from '../../core/data/item-template-data.serv
|
||||
import { getCollectionEditRoute } from '../collection-page-routing-paths';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||
import { AlertType } from '../../shared/alert/aletr-type';
|
||||
import { AlertType } from '../../shared/alert/alert-type';
|
||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||
|
||||
@Component({
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div class="container">
|
||||
<h2>{{ 'communityList.title' | translate }}</h2>
|
||||
<h1>{{ 'communityList.title' | translate }}</h1>
|
||||
<ds-themed-community-list></ds-themed-community-list>
|
||||
</div>
|
||||
|
@@ -24,8 +24,9 @@ import { FlatNode } from './flat-node.model';
|
||||
import { ShowMoreFlatNode } from './show-more-flat-node.model';
|
||||
import { FindListOptions } from '../core/data/find-list-options.model';
|
||||
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// Helper method to combine an flatten an array of observables of flatNode arrays
|
||||
// Helper method to combine and flatten an array of observables of flatNode arrays
|
||||
export const combineAndFlatten = (obsList: Observable<FlatNode[]>[]): Observable<FlatNode[]> =>
|
||||
observableCombineLatest([...obsList]).pipe(
|
||||
map((matrix: any[][]) => [].concat(...matrix)),
|
||||
@@ -186,7 +187,7 @@ export class CommunityListService {
|
||||
return this.transformCommunity(community, level, parent, expandedNodes);
|
||||
});
|
||||
if (currentPage < listOfPaginatedCommunities.totalPages && currentPage === listOfPaginatedCommunities.currentPage) {
|
||||
obsList = [...obsList, observableOf([showMoreFlatNode('community', level, parent)])];
|
||||
obsList = [...obsList, observableOf([showMoreFlatNode(`community-${uuidv4()}`, level, parent)])];
|
||||
}
|
||||
|
||||
return combineAndFlatten(obsList);
|
||||
@@ -199,7 +200,7 @@ export class CommunityListService {
|
||||
* Transforms a community in a list of FlatNodes containing firstly a flatnode of the community itself,
|
||||
* followed by flatNodes of its possible subcommunities and collection
|
||||
* It gets called recursively for each subcommunity to add its subcommunities and collections to the list
|
||||
* Number of subcommunities and collections added, is dependant on the current page the parent is at for respectively subcommunities and collections.
|
||||
* Number of subcommunities and collections added, is dependent on the current page the parent is at for respectively subcommunities and collections.
|
||||
* @param community Community being transformed
|
||||
* @param level Depth of the community in the list, subcommunities and collections go one level deeper
|
||||
* @param parent Flatnode of the parent community
|
||||
@@ -257,7 +258,7 @@ export class CommunityListService {
|
||||
let nodes = rd.payload.page
|
||||
.map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode));
|
||||
if (currentCollectionPage < rd.payload.totalPages && currentCollectionPage === rd.payload.currentPage) {
|
||||
nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)];
|
||||
nodes = [...nodes, showMoreFlatNode(`collection-${uuidv4()}`, level + 1, communityFlatNode)];
|
||||
}
|
||||
return nodes;
|
||||
} else {
|
||||
@@ -275,7 +276,7 @@ export class CommunityListService {
|
||||
|
||||
/**
|
||||
* Checks if a community has subcommunities or collections by querying the respective services with a pageSize = 0
|
||||
* Returns an observable that combines the result.payload.totalElements fo the observables that the
|
||||
* Returns an observable that combines the result.payload.totalElements of the observables that the
|
||||
* respective services return when queried
|
||||
* @param community Community being checked whether it is expandable (if it has subcommunities or collections)
|
||||
*/
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<ds-themed-loading *ngIf="(dataSource.loading$ | async) && !loadingNode" class="ds-themed-loading"></ds-themed-loading>
|
||||
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||||
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl" [trackBy]="trackBy">
|
||||
<!-- This is the tree node template for show more node -->
|
||||
<cdk-tree-node *cdkTreeNodeDef="let node; when: isShowMore" cdkTreeNodePadding
|
||||
class="example-tree-node show-more-node">
|
||||
@@ -8,7 +8,7 @@
|
||||
<span class="fa fa-chevron-right invisible" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="align-middle pt-2">
|
||||
<button *ngIf="node!==loadingNode" (click)="getNextPage(node)"
|
||||
<button *ngIf="!(dataSource.loading$ | async)" (click)="getNextPage(node)"
|
||||
class="btn btn-outline-primary btn-sm" role="button">
|
||||
<i class="fas fa-angle-down"></i> {{ 'communityList.showMore' | translate }}
|
||||
</button>
|
||||
@@ -34,13 +34,13 @@
|
||||
aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="d-flex flex-row">
|
||||
<h5 class="align-middle pt-2">
|
||||
<span class="align-middle pt-2 lead">
|
||||
<a [routerLink]="node.route" class="lead">
|
||||
{{ dsoNameService.getName(node.payload) }}
|
||||
</a>
|
||||
<span class="pr-2"> </span>
|
||||
<span *ngIf="node.payload.archivedItemsCount >= 0" class="badge badge-pill badge-secondary align-top archived-items-lead">{{node.payload.archivedItemsCount}}</span>
|
||||
</h5>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ds-truncatable [id]="node.id">
|
||||
|
@@ -17,6 +17,7 @@ import { By } from '@angular/platform-browser';
|
||||
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { FlatNode } from '../flat-node.model';
|
||||
import { RouterLinkWithHref } from '@angular/router';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
describe('CommunityListComponent', () => {
|
||||
let component: CommunityListComponent;
|
||||
@@ -138,7 +139,7 @@ describe('CommunityListComponent', () => {
|
||||
}
|
||||
if (expandedNodes === null || isEmpty(expandedNodes)) {
|
||||
if (showMoreTopComNode) {
|
||||
return observableOf([...mockTopFlatnodesUnexpanded.slice(0, endPageIndex), showMoreFlatNode('community', 0, null)]);
|
||||
return observableOf([...mockTopFlatnodesUnexpanded.slice(0, endPageIndex), showMoreFlatNode(`community-${uuidv4()}`, 0, null)]);
|
||||
} else {
|
||||
return observableOf(mockTopFlatnodesUnexpanded.slice(0, endPageIndex));
|
||||
}
|
||||
@@ -165,21 +166,21 @@ describe('CommunityListComponent', () => {
|
||||
const endSubComIndex = this.pageSize * expandedParent.currentCommunityPage;
|
||||
flatnodes = [...flatnodes, ...subComFlatnodes.slice(0, endSubComIndex)];
|
||||
if (subComFlatnodes.length > endSubComIndex) {
|
||||
flatnodes = [...flatnodes, showMoreFlatNode('community', topNode.level + 1, expandedParent)];
|
||||
flatnodes = [...flatnodes, showMoreFlatNode(`community-${uuidv4()}`, topNode.level + 1, expandedParent)];
|
||||
}
|
||||
}
|
||||
if (isNotEmpty(collFlatnodes)) {
|
||||
const endColIndex = this.pageSize * expandedParent.currentCollectionPage;
|
||||
flatnodes = [...flatnodes, ...collFlatnodes.slice(0, endColIndex)];
|
||||
if (collFlatnodes.length > endColIndex) {
|
||||
flatnodes = [...flatnodes, showMoreFlatNode('collection', topNode.level + 1, expandedParent)];
|
||||
flatnodes = [...flatnodes, showMoreFlatNode(`collection-${uuidv4()}`, topNode.level + 1, expandedParent)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (showMoreTopComNode) {
|
||||
flatnodes = [...flatnodes, showMoreFlatNode('community', 0, null)];
|
||||
flatnodes = [...flatnodes, showMoreFlatNode(`community-${uuidv4()}`, 0, null)];
|
||||
}
|
||||
return observableOf(flatnodes);
|
||||
}
|
||||
|
@@ -28,10 +28,9 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
||||
treeControl = new FlatTreeControl<FlatNode>(
|
||||
(node: FlatNode) => node.level, (node: FlatNode) => true
|
||||
);
|
||||
|
||||
dataSource: CommunityListDatasource;
|
||||
|
||||
paginationConfig: FindListOptions;
|
||||
trackBy = (index, node: FlatNode) => node.id;
|
||||
|
||||
constructor(
|
||||
protected communityListService: CommunityListService,
|
||||
@@ -58,24 +57,34 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
||||
this.communityListService.saveCommunityListStateToStore(this.expandedNodes, this.loadingNode);
|
||||
}
|
||||
|
||||
// whether or not this node has children (subcommunities or collections)
|
||||
/**
|
||||
* Whether this node has children (subcommunities or collections)
|
||||
* @param _
|
||||
* @param node
|
||||
*/
|
||||
hasChild(_: number, node: FlatNode) {
|
||||
return node.isExpandable$;
|
||||
}
|
||||
|
||||
// whether or not it is a show more node (contains no data, but is indication that there are more topcoms, subcoms or collections
|
||||
/**
|
||||
* Whether this is a show more node that contains no data, but indicates that there is
|
||||
* one or more community or collection.
|
||||
* @param _
|
||||
* @param node
|
||||
*/
|
||||
isShowMore(_: number, node: FlatNode) {
|
||||
return node.isShowMoreNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the expanded variable of a node, adds it to the expanded 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
|
||||
*/
|
||||
toggleExpanded(node: FlatNode) {
|
||||
this.loadingNode = node;
|
||||
if (node.isExpanded) {
|
||||
this.expandedNodes = this.expandedNodes.filter((node2) => node2.name !== node.name);
|
||||
this.expandedNodes = this.expandedNodes.filter((node2) => node2.id !== node.id);
|
||||
node.isExpanded = false;
|
||||
} else {
|
||||
this.expandedNodes.push(node);
|
||||
@@ -92,26 +101,28 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* Makes sure the next page of a node is added to the tree (top community, sub community of collection)
|
||||
* > Finds its parent (if not top community) and increases its corresponding collection/subcommunity currentPage
|
||||
* > Reloads tree with new page added to corresponding top community lis, sub community list or collection list
|
||||
* @param node The show more node indicating whether it's an increase in top communities, sub communities or collections
|
||||
* > Finds its parent (if not top community) and increases its corresponding collection/subcommunity
|
||||
* currentPage
|
||||
* > Reloads tree with new page added to corresponding top community lis, sub community list or
|
||||
* collection list
|
||||
* @param node The show more node indicating whether it's an increase in top communities, sub communities
|
||||
* or collections
|
||||
*/
|
||||
getNextPage(node: FlatNode): void {
|
||||
this.loadingNode = node;
|
||||
if (node.parent != null) {
|
||||
if (node.id === 'collection') {
|
||||
if (node.id.startsWith('collection')) {
|
||||
const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id);
|
||||
parentNodeInExpandedNodes.currentCollectionPage++;
|
||||
}
|
||||
if (node.id === 'community') {
|
||||
if (node.id.startsWith('community')) {
|
||||
const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id);
|
||||
parentNodeInExpandedNodes.currentCommunityPage++;
|
||||
}
|
||||
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
||||
} else {
|
||||
this.paginationConfig.currentPage++;
|
||||
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
||||
}
|
||||
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* The show more links in the community tree are also represented by a flatNode so we know where in
|
||||
* the tree it should be rendered an who its parent is (needed for the action resulting in clicking this link)
|
||||
* the tree it should be rendered and who its parent is (needed for the action resulting in clicking this link)
|
||||
*/
|
||||
export class ShowMoreFlatNode {
|
||||
}
|
||||
|
@@ -21,9 +21,6 @@
|
||||
</ds-comcol-page-content>
|
||||
</header>
|
||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||
<div class="pl-2 space-children-mr">
|
||||
<ds-dso-page-subscription-button [dso]="communityPayload"></ds-dso-page-subscription-button>
|
||||
</div>
|
||||
</div>
|
||||
<section class="comcol-page-browse-section">
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div class="row">
|
||||
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate}}</h2>
|
||||
<h1 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate}}</h1>
|
||||
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}</p>
|
||||
<div class="form-group row">
|
||||
<div class="col text-right space-children-mr">
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="container">
|
||||
<h3>{{'community.curate.header' |translate:{community: (communityName$ |async)} }}</h3>
|
||||
<h2>{{'community.curate.header' |translate:{community: (communityName$ |async)} }}</h2>
|
||||
<ds-curation-form
|
||||
[dsoHandle]="(dsoRD$|async)?.payload.handle"
|
||||
></ds-curation-form>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs';
|
||||
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
@@ -50,6 +50,8 @@ export class CommunityPageSubCollectionListComponent implements OnInit, OnDestro
|
||||
*/
|
||||
subCollectionsRDObs: BehaviorSubject<RemoteData<PaginatedList<Collection>>> = new BehaviorSubject<RemoteData<PaginatedList<Collection>>>({} as any);
|
||||
|
||||
subscriptions: Subscription[] = [];
|
||||
|
||||
constructor(
|
||||
protected cds: CollectionDataService,
|
||||
protected paginationService: PaginationService,
|
||||
@@ -77,7 +79,7 @@ export class CommunityPageSubCollectionListComponent implements OnInit, OnDestro
|
||||
const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config);
|
||||
const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig);
|
||||
|
||||
observableCombineLatest([pagination$, sort$]).pipe(
|
||||
this.subscriptions.push(observableCombineLatest([pagination$, sort$]).pipe(
|
||||
switchMap(([currentPagination, currentSort]) => {
|
||||
return this.cds.findByParent(this.community.id, {
|
||||
currentPage: currentPagination.currentPage,
|
||||
@@ -87,11 +89,12 @@ export class CommunityPageSubCollectionListComponent implements OnInit, OnDestro
|
||||
})
|
||||
).subscribe((results) => {
|
||||
this.subCollectionsRDObs.next(results);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
this.paginationService.clearPagination(this.config?.id);
|
||||
this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user