Merge remote-tracking branch 'origin/main' into w2p-119612_export-item-limit

This commit is contained in:
Jens Vannerum
2025-04-29 10:54:22 +02:00
1452 changed files with 198960 additions and 42966 deletions

View File

@@ -12,7 +12,6 @@
"eslint-plugin-rxjs",
"eslint-plugin-simple-import-sort",
"eslint-plugin-import-newlines",
"eslint-plugin-jsonc",
"dspace-angular-ts",
"dspace-angular-html"
],
@@ -294,7 +293,8 @@
"rules": {
// Custom DSpace Angular rules
"dspace-angular-html/themed-component-usages": "error",
"dspace-angular-html/no-disabled-attribute-on-button": "error"
"dspace-angular-html/no-disabled-attribute-on-button": "error",
"@angular-eslint/template/prefer-control-flow": "error"
}
},
{
@@ -302,10 +302,13 @@
"*.json5"
],
"extends": [
"plugin:jsonc/recommended-with-jsonc"
"plugin:jsonc/recommended-with-json5"
],
"rules": {
"no-irregular-whitespace": "error",
// The ESLint core no-irregular-whitespace rule doesn't work well in JSON
// See: https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-irregular-whitespace.html
"no-irregular-whitespace": "off",
"jsonc/no-irregular-whitespace": "error",
"no-trailing-spaces": "error",
"jsonc/comma-dangle": [
"error",

View File

@@ -190,12 +190,84 @@ jobs:
# Get homepage and verify that the <meta name="title"> tag includes "DSpace".
# If it does, then SSR is working, as this tag is created by our MetadataService.
# This step also prints entire HTML of homepage for easier debugging if grep fails.
- name: Verify SSR (server-side rendering)
- name: Verify SSR (server-side rendering) on Homepage
run: |
result=$(wget -O- -q http://127.0.0.1:4000/home)
echo "$result"
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep DSpace
# Get a specific community in our test data and verify that the "<h1>" tag includes "Publications" (the community name).
# If it does, then SSR is working.
- name: Verify SSR on a Community page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/communities/0958c910-2037-42a9-81c7-dca80e3892b4)
echo "$result"
echo "$result" | grep -oE "<h1 [^>]*>[^><]*</h1>" | grep Publications
# Get a specific collection in our test data and verify that the "<h1>" tag includes "Articles" (the collection name).
# If it does, then SSR is working.
- name: Verify SSR on a Collection page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/collections/282164f5-d325-4740-8dd1-fa4d6d3e7200)
echo "$result"
echo "$result" | grep -oE "<h1 [^>]*>[^><]*</h1>" | grep Articles
# Get a specific publication in our test data and verify that the <meta name="title"> tag includes
# the title of this publication. If it does, then SSR is working.
- name: Verify SSR on a Publication page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/entities/publication/6160810f-1e53-40db-81ef-f6621a727398)
echo "$result"
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "An Economic Model of Mortality Salience"
# Get a specific person in our test data and verify that the <meta name="title"> tag includes
# the name of the person. If it does, then SSR is working.
- name: Verify SSR on a Person page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/entities/person/b1b2c768-bda1-448a-a073-fc541e8b24d9)
echo "$result"
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Simmons, Cameron"
# Get a specific project in our test data and verify that the <meta name="title"> tag includes
# the name of the project. If it does, then SSR is working.
- name: Verify SSR on a Project page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/entities/project/46ccb608-a74c-4bf6-bc7a-e29cc7defea9)
echo "$result"
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "University Research Fellowship"
# Get a specific orgunit in our test data and verify that the <meta name="title"> tag includes
# the name of the orgunit. If it does, then SSR is working.
- name: Verify SSR on an OrgUnit page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/entities/orgunit/9851674d-bd9a-467b-8d84-068deb568ccf)
echo "$result"
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Law and Development"
# Get a specific journal in our test data and verify that the <meta name="title"> tag includes
# the name of the journal. If it does, then SSR is working.
- name: Verify SSR on a Journal page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/entities/journal/d4af6c3e-53d0-4757-81eb-566f3b45d63a)
echo "$result"
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Environmental &amp; Architectural Phenomenology"
# Get a specific journal volume in our test data and verify that the <meta name="title"> tag includes
# the name of the volume. If it does, then SSR is working.
- name: Verify SSR on a Journal Volume page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/entities/journalvolume/07c6249f-4bf7-494d-9ce3-6ffdb2aed538)
echo "$result"
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Environmental &amp; Architectural Phenomenology Volume 28 (2017)"
# Get a specific journal issue in our test data and verify that the <meta name="title"> tag includes
# the name of the issue. If it does, then SSR is working.
- name: Verify SSR on a Journal Issue page
run: |
result=$(wget -O- -q http://127.0.0.1:4000/entities/journalissue/44c29473-5de2-48fa-b005-e5029aa1a50b)
echo "$result"
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Environmental &amp; Architectural Phenomenology Vol. 28, No. 1"
- name: Stop running app
run: kill -9 $(lsof -t -i:4000)

View File

@@ -58,7 +58,10 @@
"input": "src/themes/dspace/styles/theme.scss",
"inject": false,
"bundleName": "dspace-theme"
}
},
"node_modules/leaflet/dist/leaflet.css",
"node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css",
"node_modules/leaflet.markercluster/dist/MarkerCluster.css"
],
"scripts": [],
"baseHref": "/"

View File

@@ -24,7 +24,9 @@ ssr:
# disabled (false) by default to boost server performance at the expense of loading smoothness.
inlineCriticalCss: false
# Path prefixes to enable SSR for. By default these are limited to paths of primary DSpace objects.
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ]
# NOTE: The "/handle/" path ensures Handle redirects work via SSR. The "/reload/" path ensures
# hard refreshes (e.g. after login) trigger SSR while fully reloading the page.
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/', '/reload/' ]
# Whether to enable rendering of Search component on SSR.
# If set to true the component will be included in the HTML returned from the server side rendering.
# If set to false the component will not be included in the HTML returned from the server side rendering.
@@ -33,6 +35,19 @@ ssr:
# If set to true the component will be included in the HTML returned from the server side rendering.
# If set to false the component will not be included in the HTML returned from the server side rendering.
enableBrowseComponent: false
# Enable state transfer from the server-side application to the client-side application.
# Defaults to true.
# Note: When using an external application cache layer, it's recommended not to transfer the state to avoid caching it.
# Disabling it ensures that dynamic state information is not inadvertently cached, which can improve security and
# ensure that users always use the most up-to-date state.
transferState: true
# When a different REST base URL is used for the server-side application, the generated state contains references to
# REST resources with the internal URL configured. By default, these internal URLs are replaced with public URLs.
# Disable this setting to avoid URL replacement during SSR. In this the state is not transferred to avoid security issues.
replaceRestUrl: true
# Enable request performance profiling data collection and printing the results in the server console.
# Defaults to false. Enabling in production is NOT recommended
#enablePerformanceProfiler: false
# The REST API server settings
# NOTE: these settings define which (publicly available) REST API to use. They are usually
@@ -43,6 +58,9 @@ rest:
port: 443
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
nameSpace: /server
# Provide a different REST url to be used during SSR execution. It must contain the whole url including protocol, server port and
# server namespace (uncomment to use it).
#ssrBaseUrl: http://localhost:8080/server
# Caching settings
cache:
@@ -332,15 +350,25 @@ item:
# Rounded to the nearest size in the list of selectable sizes on the
# settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'.
pageSize: 5
# Show the bitstream access status label on the item page
showAccessStatuses: false
# Community Page Config
community:
# Default tab to be shown when browsing a Community. Valid values are: comcols, search, or browse_<field>
# <field> must be any of the configured "browse by" fields, e.g., dateissued, author, title, or subject
# When the default tab is not the 'search' tab, the search tab is moved to the last position
defaultBrowseTab: search
# Search tab config
searchSection:
showSidebar: true
# Collection Page Config
collection:
# Default tab to be shown when browsing a Collection. Valid values are: search, or browse_<field>
# <field> must be any of the configured "browse by" fields, e.g., dateissued, author, title, or subject
# When the default tab is not the 'search' tab, the search tab is moved to the last position
defaultBrowseTab: search
# Search tab config
searchSection:
showSidebar: true
@@ -518,7 +546,6 @@ notifyMetrics:
config: 'NOTIFY.outgoing.delivered'
description: 'admin-notify-dashboard.NOTIFY.outgoing.delivered.description'
# Live Region configuration
# Live Region as defined by w3c, https://www.w3.org/TR/wai-aria-1.1/#terms:
# Live regions are perceivable regions of a web page that are typically updated as a
@@ -532,3 +559,37 @@ liveRegion:
messageTimeOutDurationMs: 30000
# The visibility of the live region. Setting this to true is only useful for debugging purposes.
isVisible: false
# Geospatial Map display options
geospatialMapViewer:
# Which fields to use for parsing as geospatial points in search maps
# (note, the item page field component allows any field(s) to be used
# and is set as an input when declaring the component)
spatialMetadataFields:
- 'dcterms.spatial'
# Which discovery configuration to use for 'geospatial search', used
# in the browse map
spatialFacetDiscoveryConfiguration: 'geospatial'
# Which filter / facet name to use for faceted geospatial search
# used in the browse map
spatialPointFilterName: 'point'
# Whether item page geospatial metadata should be displayed
# (assumes they are wrapped in a test for this config in the template as
# per the default templates supplied with DSpace for untyped-item and publication)
enableItemPageFields: false
# Whether the browse map should be enabled and included in the browse menu
enableBrowseMap: false
# Whether a 'map view' mode should be included alongside list and grid views
# in search result pages
enableSearchViewMode: false
# The tile provider(s) to use for the map tiles drawn in the leaflet maps.
# (see https://leaflet-extras.github.io/leaflet-providers/preview/) for a full list
tileProviders:
- 'OpenStreetMap.Mapnik'
# Starting centre point for the map, as lat and lng coordinates. This is useful
# to set the centre of the map when the map is first loaded and if there are no
# points, shapes or markers to display.
# Defaults to the centre of Istanbul
defaultCentrePoint:
lat: 41.015137
lng: 28.979530

View File

@@ -1,5 +1,4 @@
import { testA11y } from 'cypress/support/utils';
import { Options } from 'cypress-axe';
describe('Admin Sidebar', () => {
beforeEach(() => {
@@ -16,13 +15,6 @@ describe('Admin Sidebar', () => {
cy.get('ds-expandable-admin-sidebar-section').click({ multiple: true });
// Analyze <ds-admin-sidebar> for accessibility
testA11y('ds-admin-sidebar',
{
rules: {
// Currently all expandable sections have nested interactive elements
// See https://github.com/DSpace/dspace-angular/issues/2178
'nested-interactive': { enabled: false },
},
} as Options);
testA11y('ds-admin-sidebar');
});
});

View File

@@ -12,6 +12,13 @@ describe('Community List Page', () => {
cy.get('[data-test="expand-button"]').click({ multiple: true });
// Analyze <ds-community-list-page> for accessibility issues
testA11y('ds-community-list-page');
testA11y('ds-community-list-page', {
rules: {
// When expanding a cdk node on the community-list page, the 'aria-posinset' property becomes 0.
// 0 is not a valid value for 'aria-posinset' so the test fails.
// see https://github.com/DSpace/dspace-angular/issues/4068
'aria-valid-attr-value': { enabled: false },
},
});
});
});

View File

@@ -7,7 +7,7 @@ describe('Item Statistics Page', () => {
it('should load if you click on "Statistics" from an Item/Entity page', () => {
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
cy.location('pathname').should('eq', '/statistics/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
});
it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => {

View File

@@ -217,7 +217,7 @@ describe('New Submission page', () => {
});
// Close popup window
cy.get('ds-dynamic-lookup-relation-modal button.close').click();
cy.get('ds-dynamic-lookup-relation-modal button.btn-close').click();
// Back on the form, click the discard button to remove new submission
// Clicking it will display a confirmation, which we will confirm with another click

View File

@@ -1,10 +1,16 @@
{
"extends": "../tsconfig.json",
"include": [
"**/*.ts"
"**/*.ts",
"../cypress.config.ts"
],
"compilerOptions": {
"sourceMap": false,
"typeRoots": [
"../node_modules",
"../node_modules/@types",
"../src/typings.d.ts"
],
"types": [
"cypress",
"cypress-axe",

View File

@@ -93,7 +93,10 @@ services:
volumes:
# Keep Solr data directory between reboots
- solr_data:/var/solr/data
# Initialize all DSpace Solr cores using the mounted configsets (see above), then start Solr
# NOTE: We are not running Solr as "root", but we need root permissions to copy our cores to the mounted
# /var/solr/data directory. Then we start Solr as the "solr" user.
user: root
# Initialize all DSpace Solr cores, then start Solr
entrypoint:
- /bin/bash
- '-c'
@@ -111,7 +114,8 @@ services:
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
exec solr -f
chown -R solr:solr /var/solr
runuser -u solr -- solr-foreground
volumes:
assetstore:
pgdata:

View File

@@ -97,11 +97,16 @@ services:
volumes:
# Keep Solr data directory between reboots
- solr_data:/var/solr/data
# NOTE: We are not running Solr as "root", but we need root permissions to copy our cores to the mounted
# /var/solr/data directory. Then we start Solr as the "solr" user.
user: root
# Initialize all DSpace Solr cores using the mounted local configsets (see above), then start Solr
# * First, run precreate-core to create the core (if it doesn't yet exist). If exists already, this is a no-op
# * Second, copy configsets to this core:
# Updates to Solr configs require the container to be rebuilt/restarted:
# `docker compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d --build dspacesolr`
# * Third, ensure all new folders are owned by "solr" user
# * Finally, start Solr as the "solr" user via the provided solr-foreground script
entrypoint:
- /bin/bash
- '-c'
@@ -119,7 +124,8 @@ services:
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
exec solr -f
chown -R solr:solr /var/solr
runuser -u solr -- solr-foreground
volumes:
assetstore:
pgdata:

12606
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -57,76 +57,74 @@
"private": true,
"overrides": {
"@kolkov/ngx-gallery": {
"@angular/animations": "^17.3.11",
"@angular/common": "^17.3.11",
"@angular/core": "^17.3.11"
"@angular/animations": "^18.2.12",
"@angular/common": "^18.2.12",
"@angular/core": "^18.2.12"
},
"@ng-bootstrap/ng-bootstrap": {
"@angular/common": "^17.3.11",
"@angular/core": "^17.3.11",
"@angular/forms": "^17.3.11",
"@angular/localize": "^17.3.11"
"@angular/common": "^18.2.12",
"@angular/core": "^18.2.12",
"@angular/forms": "^18.2.12",
"@angular/localize": "^18.2.12"
},
"@ng-dynamic-forms/core": {
"@angular/common": "^17.3.11",
"@angular/core": "^17.3.11",
"@angular/forms": "^17.3.11"
"@angular/common": "^18.2.12",
"@angular/core": "^18.2.12",
"@angular/forms": "^18.2.12"
},
"@ng-dynamic-forms/ui-ng-bootstrap": {
"ngx-mask": "14.2.4"
},
"@ngtools/webpack": {
"@angular/compiler-cli": "^17.3.11",
"typescript": "~5.4.5"
"ngx-mask": "14.2.4",
"@ng-bootstrap/ng-bootstrap": "^12.0.0",
"bootstrap": "^5.3"
},
"@nicky-lenaers/ngx-scroll-to": {
"@angular/common": "^17.3.11",
"@angular/core": "^17.3.11"
"@angular/common": "^18.2.12",
"@angular/core": "^18.2.12"
},
"eslint-plugin-unused-imports": {
"@typescript-eslint/eslint-plugin": "^7.2.0"
},
"ng2-file-upload": {
"@angular/common": "^17.3.11",
"@angular/core": "^17.3.11"
},
"ngx-infinite-scroll": {
"@angular/common": "^17.3.11",
"@angular/core": "^17.3.11"
}
"@angular/common": "^18.2.12",
"@angular/core": "^18.2.12"
},
"notistack": "3.0.1"
},
"dependencies": {
"@angular/animations": "^17.3.12",
"@angular/cdk": "^17.3.10",
"@angular/common": "^17.3.12",
"@angular/compiler": "^17.3.12",
"@angular/core": "^17.3.12",
"@angular/forms": "^17.3.12",
"@angular/localize": "^17.3.12",
"@angular/platform-browser": "^17.3.12",
"@angular/platform-browser-dynamic": "^17.3.12",
"@angular/platform-server": "^17.3.12",
"@angular/router": "^17.3.12",
"@angular/ssr": "^17.3.11",
"@babel/runtime": "7.26.0",
"@angular/animations": "^18.2.12",
"@angular/cdk": "^18.2.12",
"@angular/common": "^18.2.12",
"@angular/compiler": "^18.2.12",
"@angular/core": "^18.2.12",
"@angular/forms": "^18.2.12",
"@angular/localize": "^18.2.12",
"@angular/platform-browser": "^18.2.12",
"@angular/platform-browser-dynamic": "^18.2.12",
"@angular/platform-server": "^18.2.12",
"@angular/router": "^18.2.12",
"@angular/ssr": "^18.2.18",
"@babel/runtime": "7.27.0",
"@kolkov/ngx-gallery": "^2.0.1",
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
"@ng-bootstrap/ng-bootstrap": "^12.0.0",
"@ng-dynamic-forms/core": "^16.0.0",
"@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0",
"@ngrx/effects": "^17.1.1",
"@ngrx/router-store": "^17.1.1",
"@ngrx/store": "^17.1.1",
"@ngx-translate/core": "^14.0.0",
"@ngrx/effects": "^18.1.1",
"@ngrx/operators": "^18.0.0",
"@ngrx/router-store": "^18.1.1",
"@ngrx/store": "^18.1.1",
"@ngx-translate/core": "^16.0.3",
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
"@terraformer/wkt": "^2.2.1",
"altcha": "^0.9.0",
"angulartics2": "^12.2.0",
"axios": "^1.7.9",
"bootstrap": "^4.6.1",
"axios": "^1.8.4",
"bootstrap": "^5.3",
"cerialize": "0.1.18",
"cli-progress": "^3.12.0",
"colors": "^1.4.0",
"compression": "^1.7.5",
"cookie-parser": "1.4.7",
"core-js": "^3.40.0",
"core-js": "^3.41.0",
"date-fns": "^2.29.3",
"date-fns-tz": "^1.3.7",
"deepmerge": "^4.3.1",
@@ -134,28 +132,32 @@
"express": "^4.21.2",
"express-rate-limit": "^5.1.3",
"fast-json-patch": "^3.1.1",
"filesize": "^6.1.0",
"http-proxy-middleware": "^2.0.7",
"filesize": "^10.1.6",
"http-proxy-middleware": "^2.0.9",
"http-terminator": "^3.2.0",
"isbot": "^5.1.21",
"isbot": "^5.1.27",
"js-cookie": "2.2.1",
"js-yaml": "^4.1.0",
"json5": "^2.2.3",
"jsonschema": "1.5.0",
"jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"leaflet": "^1.9.4",
"leaflet-providers": "^2.0.0",
"leaflet.markercluster": "^1.5.3",
"lodash-es": "^4.17.21",
"lru-cache": "^7.14.1",
"markdown-it": "^13.0.1",
"mirador": "^3.4.3",
"mirador-dl-plugin": "^0.13.0",
"mirador-share-plugin": "^0.16.0",
"morgan": "^1.10.0",
"ng2-file-upload": "5.0.0",
"ng2-file-upload": "7.0.1",
"ng2-nouislider": "^2.0.0",
"ngx-infinite-scroll": "^16.0.0",
"ngx-infinite-scroll": "^18.0.0",
"ngx-matomo-client": "^6.4.1",
"ngx-pagination": "6.0.3",
"ngx-skeleton-loader": "^9.0.0",
"ngx-ui-switch": "^14.1.0",
"ngx-ui-switch": "^15.0.0",
"nouislider": "^15.7.1",
"orejime": "^2.3.1",
"pem": "1.14.8",
@@ -165,29 +167,29 @@
"zone.js": "~0.14.10"
},
"devDependencies": {
"@angular-builders/custom-webpack": "~17.0.2",
"@angular-devkit/build-angular": "^17.3.11",
"@angular-eslint/builder": "^17.5.3",
"@angular-eslint/bundled-angular-compiler": "^17.5.3",
"@angular-eslint/eslint-plugin": "^17.5.3",
"@angular-eslint/eslint-plugin-template": "^17.5.3",
"@angular-eslint/schematics": "^17.5.3",
"@angular-eslint/template-parser": "^17.5.3",
"@angular-eslint/utils": "^17.5.3",
"@angular/cli": "^17.3.11",
"@angular/compiler-cli": "^17.3.11",
"@angular/language-service": "^17.3.12",
"@angular-builders/custom-webpack": "~18.0.0",
"@angular-devkit/build-angular": "^18.2.18",
"@angular-eslint/builder": "^18.4.1",
"@angular-eslint/bundled-angular-compiler": "^18.4.1",
"@angular-eslint/eslint-plugin": "^18.4.1",
"@angular-eslint/eslint-plugin-template": "^18.4.1",
"@angular-eslint/schematics": "^18.4.1",
"@angular-eslint/template-parser": "^18.4.1",
"@angular-eslint/utils": "^18.4.1",
"@angular/cli": "^18.2.18",
"@angular/compiler-cli": "^18.2.12",
"@angular/language-service": "^18.2.12",
"@cypress/schematic": "^1.5.0",
"@fortawesome/fontawesome-free": "^6.7.2",
"@ngrx/store-devtools": "^17.1.1",
"@ngtools/webpack": "^16.2.16",
"@ngrx/store-devtools": "^18.1.1",
"@ngtools/webpack": "^18.2.19",
"@types/deep-freeze": "0.1.5",
"@types/ejs": "^3.1.2",
"@types/express": "^4.17.17",
"@types/grecaptcha": "^3.0.9",
"@types/jasmine": "~3.6.0",
"@types/js-cookie": "2.2.6",
"@types/lodash": "^4.17.14",
"@types/lodash-es": "^4.17.12",
"@types/node": "^14.14.9",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
@@ -198,7 +200,7 @@
"copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.3",
"cypress": "^13.17.0",
"cypress-axe": "^1.5.0",
"cypress-axe": "^1.6.0",
"deep-freeze": "0.0.1",
"eslint": "^8.39.0",
"eslint-plugin-deprecation": "^1.4.1",
@@ -207,7 +209,7 @@
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-import-newlines": "^1.3.1",
"eslint-plugin-jsdoc": "^45.0.0",
"eslint-plugin-jsonc": "^2.18.2",
"eslint-plugin-jsonc": "^2.20.0",
"eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-rxjs": "^5.0.3",
"eslint-plugin-simple-import-sort": "^10.0.0",
@@ -222,7 +224,7 @@
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"karma-mocha-reporter": "2.2.5",
"ng-mocks": "^14.13.2",
"ng-mocks": "^14.13.4",
"ngx-mask": "14.2.4",
"nodemon": "^2.0.22",
"postcss": "^8.5",
@@ -230,12 +232,12 @@
"postcss-loader": "^4.0.3",
"postcss-preset-env": "^7.4.2",
"rimraf": "^3.0.2",
"sass": "~1.83.4",
"sass": "~1.86.3",
"sass-loader": "^12.6.0",
"sass-resources-loader": "^2.2.5",
"ts-node": "^8.10.2",
"typescript": "~5.4.5",
"webpack": "5.97.1",
"webpack": "5.99.7",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
}

View File

@@ -20,10 +20,10 @@ import 'reflect-metadata';
/* eslint-disable import/no-namespace */
import * as morgan from 'morgan';
import * as express from 'express';
import express from 'express';
import * as ejs from 'ejs';
import * as compression from 'compression';
import * as expressStaticGzip from 'express-static-gzip';
import expressStaticGzip from 'express-static-gzip';
/* eslint-enable import/no-namespace */
import axios from 'axios';
import LRU from 'lru-cache';
@@ -81,6 +81,9 @@ let anonymousCache: LRU<string, any>;
// extend environment with app config for server
extendEnvironmentWithAppConfig(environment, appConfig);
// The REST server base URL
const REST_BASE_URL = environment.rest.ssrBaseUrl || environment.rest.baseUrl;
// The Express app is exported so that it can be used by serverless Functions.
export function app() {
@@ -156,7 +159,7 @@ export function app() {
* Proxy the sitemaps
*/
router.use('/sitemap**', createProxyMiddleware({
target: `${environment.rest.baseUrl}/sitemaps`,
target: `${REST_BASE_URL}/sitemaps`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true,
}));
@@ -165,7 +168,7 @@ export function app() {
* Proxy the linksets
*/
router.use('/signposting**', createProxyMiddleware({
target: `${environment.rest.baseUrl}`,
target: `${REST_BASE_URL}`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true,
}));
@@ -266,6 +269,11 @@ function serverSideRender(req, res, next, sendToUser: boolean = true) {
})
.then((html) => {
if (hasValue(html)) {
// Replace REST URL with UI URL
if (environment.ssr.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) {
html = html.replace(new RegExp(REST_BASE_URL, 'g'), environment.rest.baseUrl);
}
// save server side rendered page to cache (if any are enabled)
saveToCache(req, html);
if (sendToUser) {
@@ -623,7 +631,7 @@ function start() {
* The callback function to serve health check requests
*/
function healthCheck(req, res) {
const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`;
const baseUrl = `${REST_BASE_URL}${environment.actuators.endpointPath}`;
axios.get(baseUrl)
.then((response) => {
res.status(response.status).send(response.data);

View File

@@ -1,19 +1,13 @@
<ngb-accordion #acc="ngbAccordion" [activeIds]="'browse'">
<ngb-panel [id]="'browse'">
<ng-template ngbPanelHeader>
<div class="w-100 d-flex gap-3 justify-content-between collapse-toggle" ngbPanelToggle (click)="acc.toggle('browse')"
data-test="browse">
<ng-template ngbPanelTitle>
<div class="w-100 d-flex gap-3 justify-content-between collapse-toggle" (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="bulk-access-browse-panel-content">
[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 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>
</div>
</div>
</ng-template>
<ng-template ngbPanelContent>
@@ -22,11 +16,11 @@
<li [ngbNavItem]="'search'" role="presentation">
<a ngbNavLink>{{'admin.access-control.bulk-access-browse.search.header' | translate}}</a>
<ng-template ngbNavContent>
<div class="mx-n3">
<div class="bulk-access-search">
<ds-search [configuration]="'administrativeBulkAccess'"
[selectable]="true"
[selectionConfig]="{ repeatable: true, listId: listId }"
[showThumbnails]="false"></ds-search>
[selectable]="true"
[selectionConfig]="{ repeatable: true, listId: listId }"
[showThumbnails]="false"></ds-search>
</div>
</ng-template>
</li>
@@ -42,21 +36,25 @@
[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>
@if ((objectsSelected$|async)?.hasSucceeded) {
<ul class="list-unstyled ms-4">
@for (object of (objectsSelected$|async)?.payload?.page | paginate: { itemsPerPage: (paginationOptions$ | async).pageSize,
currentPage: (paginationOptions$ | async).currentPage, totalItems: (objectsSelected$|async)?.payload?.page.length }; track object; let i = $index; let last = $last) {
<li
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>

View File

@@ -0,0 +1,4 @@
.bulk-access-search {
margin-right: calc(var(--bs-gutter-x, 1.5rem) / -2);
margin-left: calc(var(--bs-gutter-x, 1.5rem) / -2);
}

View File

@@ -1,8 +1,4 @@
import {
AsyncPipe,
NgForOf,
NgIf,
} from '@angular/common';
import { AsyncPipe } from '@angular/common';
import {
Component,
Input,
@@ -59,11 +55,9 @@ import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
AsyncPipe,
NgbAccordionModule,
TranslateModule,
NgIf,
NgbNavModule,
ThemedSearchComponent,
BrowserOnlyPipe,
NgForOf,
NgxPaginationModule,
SelectableListItemControlComponent,
ListableObjectComponentLoaderComponent,

View File

@@ -7,7 +7,7 @@
<hr>
<div class="d-flex justify-content-end">
<button class="btn btn-outline-primary mr-3" (click)="reset()">
<button class="btn btn-outline-primary me-3" (click)="reset()">
{{ 'access-control-cancel' | translate }}
</button>
<button class="btn btn-primary" [dsBtnDisabled]="!canExport()" (click)="submit()">

View File

@@ -1,4 +1,7 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
Component,
NO_ERRORS_SCHEMA,
} from '@angular/core';
import {
ComponentFixture,
TestBed,
@@ -62,10 +65,17 @@ describe('BulkAccessComponent', () => {
'file': { },
};
const mockSettings: any = jasmine.createSpyObj('AccessControlFormContainerComponent', {
getValue: jasmine.createSpy('getValue'),
reset: jasmine.createSpy('reset'),
});
@Component({
selector: 'ds-bulk-access-settings',
template: '',
exportAs: 'dsBulkSettings',
standalone: true,
})
class MockBulkAccessSettingsComponent {
isFormValid = jasmine.createSpy('isFormValid').and.returnValue(false);
getValue = jasmine.createSpy('getValue');
reset = jasmine.createSpy('reset');
}
const selection: any[] = [{ indexableObject: { uuid: '1234' } }, { indexableObject: { uuid: '5678' } }];
const selectableListState: SelectableListState = { id: 'test', selection };
const expectedIdList = ['1234', '5678'];
@@ -93,6 +103,9 @@ describe('BulkAccessComponent', () => {
BulkAccessSettingsComponent,
],
},
add: {
imports: [MockBulkAccessSettingsComponent],
},
})
.compileComponents();
});
@@ -109,13 +122,12 @@ describe('BulkAccessComponent', () => {
fixture.destroy();
});
describe('when there are no elements selected', () => {
describe('when there are no elements selected and step two form is invalid', () => {
beforeEach(() => {
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListStateEmpty));
fixture.detectChanges();
component.settings = mockSettings;
});
it('should create', () => {
@@ -138,7 +150,6 @@ describe('BulkAccessComponent', () => {
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListState));
fixture.detectChanges();
component.settings = mockSettings;
});
it('should create', () => {
@@ -149,15 +160,29 @@ describe('BulkAccessComponent', () => {
expect(component.objectsSelected$.value).toEqual(expectedIdList);
});
it('should enable the execute button when there are objects selected', () => {
it('should not enable the execute button when there are objects selected and step two form is invalid', () => {
component.objectsSelected$.next(['1234']);
expect(component.canExport()).toBe(true);
expect(component.canExport()).toBe(false);
});
it('should call the settings reset method when reset is called', () => {
component.reset();
expect(component.settings.reset).toHaveBeenCalled();
});
});
describe('when there are elements selected and the step two form is valid', () => {
beforeEach(() => {
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListState));
fixture.detectChanges();
(component as any).settings.isFormValid.and.returnValue(true);
});
it('should enable the execute button when there are objects selected and step two form is valid', () => {
component.objectsSelected$.next(['1234']);
expect(component.canExport()).toBe(true);
});
it('should call the bulkAccessControlService executeScript method when submit is called', () => {
(component.settings as any).getValue.and.returnValue(mockFormState);

View File

@@ -1,4 +1,5 @@
import {
ChangeDetectionStrategy,
Component,
OnInit,
ViewChild,
@@ -31,6 +32,7 @@ import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.com
BtnDisabledDirective,
],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BulkAccessComponent implements OnInit {
@@ -70,7 +72,7 @@ export class BulkAccessComponent implements OnInit {
}
canExport(): boolean {
return this.objectsSelected$.value?.length > 0;
return this.objectsSelected$.value?.length > 0 && this.settings?.isFormValid();
}
/**

View File

@@ -1,17 +1,11 @@
<ngb-accordion #acc="ngbAccordion" [activeIds]="'settings'">
<ngb-panel [id]="'settings'">
<ng-template ngbPanelHeader>
<ng-template ngbPanelTitle>
<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 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>
</div>
</div>
</ng-template>
<ng-template ngbPanelContent>

View File

@@ -1,4 +1,4 @@
import { NgIf } from '@angular/common';
import {
Component,
ViewChild,
@@ -16,7 +16,6 @@ import { AccessControlFormContainerComponent } from '../../../shared/access-cont
imports: [
NgbAccordionModule,
TranslateModule,
NgIf,
AccessControlFormContainerComponent,
],
standalone: true,
@@ -43,4 +42,8 @@ export class BulkAccessSettingsComponent {
this.controlForm.reset();
}
isFormValid() {
return this.controlForm.isValid();
}
}

View File

@@ -5,10 +5,10 @@
<h1 id="header" class="pb-2">{{labelPrefix + 'head' | translate}}</h1>
<div>
<button class="mr-auto btn btn-success addEPerson-button"
[routerLink]="'create'">
<button class="me-auto btn btn-success addEPerson-button"
[routerLink]="'create'">
<i class="fas fa-plus"></i>
<span class="d-none d-sm-inline ml-1">{{labelPrefix + 'button.add' | translate}}</span>
<span class="d-none d-sm-inline ms-1">{{labelPrefix + 'button.add' | translate}}</span>
</button>
</div>
</div>
@@ -18,77 +18,84 @@
</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">
<select name="scope" id="scope" formControlName="scope" class="form-select" 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">
<div class="flex-grow-1 me-3 ms-3">
<div class="mb-3 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)">
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>
<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>
class="search-button btn btn-secondary">{{labelPrefix + 'button.see-all' | translate}}</button>
</div>
</form>
<ds-loading *ngIf="searching$ | async"></ds-loading>
<ds-pagination
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && (searching$ | async) !== true"
[paginationOptions]="config"
[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' : (activeEPerson$ | async) === epersonDto.eperson}">
<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)"
@if (searching$ | async) {
<ds-loading></ds-loading>
}
@if ((pageInfoState$ | async)?.totalElements > 0 && (searching$ | async) !== true) {
<ds-pagination
[paginationOptions]="config"
[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>
@for (epersonDto of (ePeopleDto$ | async)?.page; track epersonDto) {
<tr
[ngClass]="{'table-primary' : (activeEPerson$ | async) === epersonDto.eperson}">
<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>
<i class="fas fa-edit fa-fw"></i>
</button>
@if (epersonDto.ableToDelete) {
<button (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>
}
@if ((pageInfoState$ | async)?.totalElements === 0) {
<div class="alert alert-info w-100 mb-2" role="alert">
{{labelPrefix + 'no-items' | translate}}
</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>
</div>

View File

@@ -1,8 +1,6 @@
import {
AsyncPipe,
NgClass,
NgForOf,
NgIf,
} from '@angular/common';
import {
Component,
@@ -72,13 +70,11 @@ import { EPersonFormComponent } from './eperson-form/eperson-form.component';
TranslateModule,
RouterModule,
AsyncPipe,
NgIf,
EPersonFormComponent,
ReactiveFormsModule,
ThemedLoadingComponent,
PaginationComponent,
NgClass,
NgForOf,
],
standalone: true,
})

View File

@@ -2,97 +2,111 @@
<div class="group-form row">
<div class="col-12">
<div *ngIf="activeEPerson$ | async; then editHeader; else createHeader"></div>
<ng-template #createHeader>
<h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1>
</ng-template>
<ng-template #editHeader>
@if (activeEPerson$ | async) {
<h1 class="border-bottom pb-2">{{messagePrefix + '.edit' | translate}}</h1>
</ng-template>
} @else {
<h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1>
}
<ds-form [formId]="formId"
[formModel]="formModel"
[formGroup]="formGroup"
[formLayout]="formLayout"
[displayCancel]="false"
[submitLabel]="submitLabel"
(submitForm)="onSubmit()">
[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" [dsBtnDisabled]="(canReset$ | async) !== true" type="button" (click)="resetPassword()">
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
@if (displayResetPassword) {
<div between class="btn-group">
<button class="btn btn-primary" [dsBtnDisabled]="(canReset$ | async) !== true" type="button" (click)="resetPassword()">
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
</button>
</div>
}
@if (canImpersonate$ | async) {
<div between class="btn-group ms-1">
@if (!isImpersonated) {
<button class="btn btn-primary" type="button" (click)="impersonate()">
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
</button>
}
@if (isImpersonated) {
<button 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>
}
@if (canDelete$ | async) {
<button 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>
</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>
<ds-loading [showMessage]="false" *ngIf="!formGroup"></ds-loading>
@if (!formGroup) {
<ds-loading [showMessage]="false"></ds-loading>
}
<div *ngIf="activeEPerson$ | async">
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
<ds-loading [showMessage]="false" *ngIf="groups$ | async | dsHasNoValue"></ds-loading>
<ds-pagination
*ngIf="(groups$ | async)?.payload?.totalElements > 0"
[paginationOptions]="config"
[collectionSize]="(groups$ | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(pageChange)="onPageChange($event)">
<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((group.object | async)?.payload) }}
</td>
</tr>
</tbody>
</table>
</div>
</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>
@if (activeEPerson$ | async) {
<div>
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
@if (groups$ | async | dsHasNoValue) {
<ds-loading [showMessage]="false"></ds-loading>
}
@if ((groups$ | async)?.payload?.totalElements > 0) {
<ds-pagination
[paginationOptions]="config"
[collectionSize]="(groups$ | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(pageChange)="onPageChange($event)">
<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>
@for (group of (groups$ | async)?.payload?.page; track group) {
<tr>
<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((group.object | async)?.payload) }}
</td>
</tr>
}
</tbody>
</table>
</div>
</ds-pagination>
}
@if ((groups$ | async)?.payload?.totalElements === 0) {
<div 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>
</div>

View File

@@ -1,8 +1,6 @@
import {
AsyncPipe,
NgClass,
NgFor,
NgIf,
} from '@angular/common';
import {
ChangeDetectorRef,
@@ -84,8 +82,6 @@ import { ValidateEmailNotTaken } from './validators/email-taken.validator';
templateUrl: './eperson-form.component.html',
imports: [
FormComponent,
NgIf,
NgFor,
AsyncPipe,
TranslateModule,
NgClass,

View File

@@ -2,13 +2,7 @@
<div class="group-form row">
<div class="col-12">
<div *ngIf="activeGroup$ | async; then editHeader; else createHeader"></div>
<ng-template #createHeader>
<h1 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h1>
</ng-template>
<ng-template #editHeader>
@if (activeGroup$ | async) {
<h1 class="border-bottom pb-2">
<span
*dsContextHelp="{
@@ -17,47 +11,61 @@
iconPlacement: 'right',
tooltipPlacement: ['right', 'bottom']
}"
>
>
{{messagePrefix + '.head.edit' | translate}}
</span>
</h1>
</ng-template>
} @else {
<h1 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h1>
}
<ng-container *ngIf="(activeGroup$ | async) as groupBeingEdited">
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertType.Warning"
[content]="messagePrefix + '.alert.permanent'"></ds-alert>
<ng-container *ngIf="(activeGroupLinkedDSO$ | async) as activeGroupLinkedDSO">
<ds-alert *ngIf="(canEdit$ | async) !== true" [type]="AlertType.Warning"
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName(activeGroupLinkedDSO), comcol: activeGroupLinkedDSO.type, comcolEditRolesRoute: (linkedEditRolesRoute$ | async) })">
</ds-alert>
</ng-container>
</ng-container>
@if ((activeGroup$ | async); as groupBeingEdited) {
@if (groupBeingEdited?.permanent) {
<ds-alert [type]="AlertType.Warning"
[content]="messagePrefix + '.alert.permanent'"></ds-alert>
}
@if ((activeGroupLinkedDSO$ | async); as activeGroupLinkedDSO) {
@if ((canEdit$ | async) !== true) {
<ds-alert [type]="AlertType.Warning"
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName(activeGroupLinkedDSO), comcol: activeGroupLinkedDSO.type, comcolEditRolesRoute: (linkedEditRolesRoute$ | async) })">
</ds-alert>
}
}
}
<ds-form [formId]="formId"
[formModel]="formModel"
[formGroup]="formGroup"
[formLayout]="formLayout"
[displayCancel]="false"
(submitForm)="onSubmit()">
[formModel]="formModel"
[formGroup]="formGroup"
[formLayout]="formLayout"
[displayCancel]="false"
(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 after *ngIf="(canEdit$ | async) && !(activeGroup$ | async)?.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>
</ds-form>
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
</div>
@if ((canEdit$ | async) && !(activeGroup$ | async)?.permanent) {
<div after 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>
}
</ds-form>
<ng-container *ngIf="(activeGroup$ | async) as groupBeingEdited">
<div class="mb-5">
<ds-members-list *ngIf="groupBeingEdited !== undefined"
[messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
</div>
<ds-subgroups-list *ngIf="groupBeingEdited !== undefined"
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
</ng-container>
@if ((activeGroup$ | async); as groupBeingEdited) {
<div class="mb-5">
@if (groupBeingEdited !== undefined) {
<ds-members-list
[messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
}
</div>
@if (groupBeingEdited !== undefined) {
<ds-subgroups-list
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
}
}
</div>
</div>
</div>
</div>

View File

@@ -1,7 +1,4 @@
import {
AsyncPipe,
NgIf,
} from '@angular/common';
import { AsyncPipe } from '@angular/common';
import {
ChangeDetectorRef,
Component,
@@ -92,7 +89,6 @@ import { ValidateGroupExists } from './validators/group-exists.validator';
imports: [
FormComponent,
AlertComponent,
NgIf,
AsyncPipe,
TranslateModule,
ContextHelpDirective,

View File

@@ -3,63 +3,70 @@
<h3>{{messagePrefix + '.headMembers' | translate}}</h3>
<ds-pagination *ngIf="(ePeopleMembersOfGroup | async)?.totalElements > 0"
[paginationOptions]="config"
[collectionSize]="(ePeopleMembersOfGroup | async)?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true">
@if ((ePeopleMembersOfGroup | async)?.totalElements > 0) {
<ds-pagination
[paginationOptions]="config"
[collectionSize]="(ePeopleMembersOfGroup | async)?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="ePeopleMembersOfGroup" 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>
@for (epersonDTO of (ePeopleMembersOfGroup | async)?.page; track epersonDTO) {
<tr>
<td class="align-middle">{{epersonDTO.eperson.id}}</td>
<td class="align-middle">
<a [routerLink]="getEPersonEditRoute(epersonDTO.eperson.id)">
{{ dsoNameService.getName(epersonDTO.eperson) }}
</a>
</td>
<td class="align-middle">
{{messagePrefix + '.table.email' | translate}}: {{ epersonDTO.eperson.email ? epersonDTO.eperson.email : '-' }}<br/>
{{messagePrefix + '.table.netid' | translate}}: {{ epersonDTO.eperson.netid ? epersonDTO.eperson.netid : '-' }}
</td>
<td class="align-middle">
<div class="btn-group edit-field">
@if (epersonDTO.ableToDelete) {
<button (click)="deleteMemberFromGroup(epersonDTO.eperson)"
[dsBtnDisabled]="actionConfig.remove.disabled"
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
<i [ngClass]="actionConfig.remove.icon"></i>
</button>
}
@if (!epersonDTO.ableToDelete) {
<button
(click)="addMemberToGroup(epersonDTO.eperson)"
[dsBtnDisabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
<i [ngClass]="actionConfig.add.icon"></i>
</button>
}
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</ds-pagination>
}
<div class="table-responsive">
<table id="ePeopleMembersOfGroup" 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 epersonDTO of (ePeopleMembersOfGroup | async)?.page">
<td class="align-middle">{{epersonDTO.eperson.id}}</td>
<td class="align-middle">
<a [routerLink]="getEPersonEditRoute(epersonDTO.eperson.id)">
{{ dsoNameService.getName(epersonDTO.eperson) }}
</a>
</td>
<td class="align-middle">
{{messagePrefix + '.table.email' | translate}}: {{ epersonDTO.eperson.email ? epersonDTO.eperson.email : '-' }}<br/>
{{messagePrefix + '.table.netid' | translate}}: {{ epersonDTO.eperson.netid ? epersonDTO.eperson.netid : '-' }}
</td>
<td class="align-middle">
<div class="btn-group edit-field">
<button (click)="deleteMemberFromGroup(epersonDTO.eperson)"
*ngIf="epersonDTO.ableToDelete"
[dsBtnDisabled]="actionConfig.remove.disabled"
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
<i [ngClass]="actionConfig.remove.icon"></i>
</button>
<button *ngIf="!epersonDTO.ableToDelete"
(click)="addMemberToGroup(epersonDTO.eperson)"
[dsBtnDisabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
<i [ngClass]="actionConfig.add.icon"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
@if ((ePeopleMembersOfGroup | async) === undefined || (ePeopleMembersOfGroup | async)?.totalElements === 0) {
<div class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-members-yet' | translate}}
</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
@@ -69,77 +76,81 @@
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">
<div class="flex-grow-1 me-3">
<div class="form-group input-group me-3">
<input type="text" name="query" id="query" formControlName="query"
class="form-control" aria-label="Search input">
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>
<button type="submit" class="search-button btn btn-primary">
<i class="fas fa-search"></i> {{ messagePrefix + '.search.button' | translate }}</button>
</span>
</div>
</div>
</div>
<div>
<button (click)="clearFormAndResetResult();"
class="btn btn-secondary">{{messagePrefix + '.button.see-all' | translate}}</button>
</div>
</form>
<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"
[collectionSize]="(ePeopleSearch | async)?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true">
@if ((ePeopleSearch | async)?.totalElements > 0) {
<ds-pagination
[paginationOptions]="configSearch"
[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>
@for (eperson of (ePeopleSearch | async)?.page; track eperson) {
<tr>
<td class="align-middle">{{eperson.id}}</td>
<td class="align-middle">
<a [routerLink]="getEPersonEditRoute(eperson.id)">
{{ 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)"
[dsBtnDisabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}">
<i [ngClass]="actionConfig.add.icon"></i>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</ds-pagination>
}
<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 [routerLink]="getEPersonEditRoute(eperson.id)">
{{ 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)"
[dsBtnDisabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}">
<i [ngClass]="actionConfig.add.icon"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
@if ((ePeopleSearch | async)?.totalElements === 0 && searchDone) {
<div
class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-items' | translate}}
</div>
}
</ds-pagination>
<div *ngIf="(ePeopleSearch | async)?.totalElements === 0 && searchDone"
class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-items' | translate}}
</div>
</ng-container>
</ng-container>

View File

@@ -1,8 +1,6 @@
import {
AsyncPipe,
NgClass,
NgForOf,
NgIf,
} from '@angular/common';
import {
Component,
@@ -109,11 +107,9 @@ export interface EPersonListActionConfig {
ContextHelpDirective,
ReactiveFormsModule,
PaginationComponent,
NgIf,
AsyncPipe,
RouterLink,
NgClass,
NgForOf,
BtnDisabledDirective,
],
standalone: true,

View File

@@ -3,51 +3,56 @@
<h4>{{messagePrefix + '.headSubgroups' | translate}}</h4>
<ds-pagination *ngIf="(subGroups$ | async)?.payload?.totalElements > 0"
[paginationOptions]="config"
[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)"
@if ((subGroups$ | async)?.payload?.totalElements > 0) {
<ds-pagination
[paginationOptions]="config"
[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>
@for (group of (subGroups$ | async)?.payload?.page; track group) {
<tr>
<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>
<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>
@if ((subGroups$ | async)?.payload?.totalElements === 0) {
<div 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="{
@@ -56,75 +61,80 @@
iconPlacement: 'right',
tooltipPlacement: ['top', 'right', 'bottom']
}"
>
>
{{messagePrefix + '.search.head' | translate}}
</span>
</h4>
<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">
<div class="flex-grow-1 me-3">
<div class="mb-3 input-group me-3">
<input type="text" name="query" id="query" formControlName="query"
class="form-control" aria-label="Search input">
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>
<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 float-right">
<button (click)="clearFormAndResetResult();" class="btn btn-secondary float-end">
{{messagePrefix + '.button.see-all' | translate}}
</button>
</div>
</form>
<ds-pagination *ngIf="(searchResults$ | async)?.payload?.totalElements > 0"
[paginationOptions]="configSearch"
[collectionSize]="(searchResults$ | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="groupsSearch" 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 class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let group of (searchResults$ | 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)="addSubgroupToGroup(group)"
@if ((searchResults$ | async)?.payload?.totalElements > 0) {
<ds-pagination
[paginationOptions]="configSearch"
[collectionSize]="(searchResults$ | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="groupsSearch" 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 class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
</tr>
</thead>
<tbody>
@for (group of (searchResults$ | async)?.payload?.page; track group) {
<tr>
<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)="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>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</ds-pagination>
<i class="fas fa-plus fa-fw"></i>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</ds-pagination>
}
<div *ngIf="(searchResults$ | async)?.payload?.totalElements === 0 && searchDone" class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-items' | translate}}
</div>
@if ((searchResults$ | async)?.payload?.totalElements === 0 && searchDone) {
<div class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-items' | translate}}
</div>
}
</ng-container>

View File

@@ -1,8 +1,4 @@
import {
AsyncPipe,
NgForOf,
NgIf,
} from '@angular/common';
import { AsyncPipe } from '@angular/common';
import {
Component,
Input,
@@ -65,12 +61,10 @@ enum SubKey {
imports: [
RouterLink,
AsyncPipe,
NgForOf,
ContextHelpDirective,
TranslateModule,
ReactiveFormsModule,
PaginationComponent,
NgIf,
],
standalone: true,
})

View File

@@ -4,21 +4,21 @@
<div class="d-flex justify-content-between border-bottom mb-3">
<h1 id="header" class="pb-2">{{messagePrefix + 'head' | translate}}</h1>
<div>
<button class="mr-auto btn btn-success"
[routerLink]="'create'">
<button class="me-auto btn btn-success"
[routerLink]="'create'">
<i class="fas fa-plus"></i>
<span class="d-none d-sm-inline ml-1">{{messagePrefix + 'button.add' | translate}}</span>
<span class="d-none d-sm-inline ms-1">{{messagePrefix + 'button.add' | translate}}</span>
</button>
</div>
</div>
<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">
<div class="flex-grow-1 me-3">
<div class="mb-3 input-group">
<input type="text" name="query" id="query" formControlName="query"
class="form-control" [attr.aria-label]="messagePrefix + 'search.placeholder' | translate"
[placeholder]="(messagePrefix + 'search.placeholder' | translate)" >
class="form-control" [attr.aria-label]="messagePrefix + 'search.placeholder' | translate"
[placeholder]="(messagePrefix + 'search.placeholder' | translate)" >
<span class="input-group-append">
<button type="submit" class="search-button btn btn-primary">
<i class="fas fa-search"></i> {{ messagePrefix + 'search.button' | translate }}
@@ -33,66 +33,78 @@
</div>
</form>
<ds-loading *ngIf="loading$ | async"></ds-loading>
<ds-pagination
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && (loading$ | async) !== true"
[paginationOptions]="config"
[collectionSize]="(pageInfoState$ | async)?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true">
@if (loading$ | async) {
<ds-loading></ds-loading>
}
@if ((pageInfoState$ | async)?.totalElements > 0 && (loading$ | async) !== true) {
<ds-pagination
[paginationOptions]="config"
[collectionSize]="(pageInfoState$ | async)?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="groups" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col">{{messagePrefix + 'table.id' | translate}}</th>
<th scope="col">{{messagePrefix + 'table.name' | translate}}</th>
<th scope="col">{{messagePrefix + 'table.collectionOrCommunity' | translate}}</th>
<th scope="col">{{messagePrefix + 'table.members' | translate}}</th>
<th>{{messagePrefix + 'table.edit' | translate}}</th>
</tr>
</thead>
<tbody>
@for (groupDto of (groupsDto$ | async)?.page; track groupDto) {
<tr>
<td>{{groupDto.group.id}}</td>
<td>{{ dsoNameService.getName(groupDto.group) }}</td>
<td>{{ dsoNameService.getName((groupDto.group.object | async)?.payload) }}</td>
<td>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td>
<td>
<div class="btn-group edit-field">
@switch (groupDto.ableToEdit) {
@case (true) {
<button
[routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
class="btn btn-outline-primary btn-sm btn-edit"
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: dsoNameService.getName(groupDto.group) } }}"
>
<i class="fas fa-edit fa-fw"></i>
</button>
}
@case (false) {
<button
[dsBtnDisabled]="true"
class="btn btn-outline-primary btn-sm btn-edit"
placement="left"
[ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate"
>
<i class="fas fa-edit fa-fw"></i>
</button>
}
}
@if (!groupDto.group?.permanent && groupDto.ableToDelete) {
<button
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm btn-delete"
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: dsoNameService.getName(groupDto.group) } }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
}
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</ds-pagination>
}
<div class="table-responsive">
<table id="groups" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col">{{messagePrefix + 'table.id' | translate}}</th>
<th scope="col">{{messagePrefix + 'table.name' | translate}}</th>
<th scope="col">{{messagePrefix + 'table.collectionOrCommunity' | translate}}</th>
<th scope="col">{{messagePrefix + 'table.members' | translate}}</th>
<th>{{messagePrefix + 'table.edit' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let groupDto of (groupsDto$ | async)?.page">
<td>{{groupDto.group.id}}</td>
<td>{{ dsoNameService.getName(groupDto.group) }}</td>
<td>{{ dsoNameService.getName((groupDto.group.object | async)?.payload) }}</td>
<td>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td>
<td>
<div class="btn-group edit-field">
<ng-container [ngSwitch]="groupDto.ableToEdit">
<button *ngSwitchCase="true"
[routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
class="btn btn-outline-primary btn-sm btn-edit"
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: dsoNameService.getName(groupDto.group) } }}"
>
<i class="fas fa-edit fa-fw"></i>
</button>
<button *ngSwitchCase="false"
[dsBtnDisabled]="true"
class="btn btn-outline-primary btn-sm btn-edit"
placement="left"
[ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate"
>
<i class="fas fa-edit fa-fw"></i>
</button>
</ng-container>
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm btn-delete"
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: dsoNameService.getName(groupDto.group) } }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
@if ((pageInfoState$ | async)?.totalElements === 0) {
<div class="alert alert-info w-100 mb-2" role="alert">
{{messagePrefix + 'no-items' | translate}}
</div>
</ds-pagination>
<div *ngIf="(pageInfoState$ | async)?.totalElements === 0" class="alert alert-info w-100 mb-2" role="alert">
{{messagePrefix + 'no-items' | translate}}
</div>
}
</div>
</div>

View File

@@ -1,10 +1,4 @@
import {
AsyncPipe,
NgForOf,
NgIf,
NgSwitch,
NgSwitchCase,
} from '@angular/common';
import { AsyncPipe } from '@angular/common';
import {
Component,
OnDestroy,
@@ -79,12 +73,8 @@ import { followLink } from '../../shared/utils/follow-link-config.model';
RouterLink,
ReactiveFormsModule,
AsyncPipe,
NgIf,
PaginationComponent,
NgSwitch,
NgSwitchCase,
NgbTooltipModule,
NgForOf,
BtnDisabledDirective,
],
standalone: true,

View File

@@ -1,14 +1,16 @@
<div class="container">
<h1 id="header">{{'admin.batch-import.page.header' | translate}}</h1>
<p>{{'admin.batch-import.page.help' | translate}}</p>
<p *ngIf="dso">
selected collection: <b>{{getDspaceObjectName()}}</b>&nbsp;
<a href="javascript:void(0)" (click)="removeDspaceObject()">{{'admin.batch-import.page.remove' | translate}}</a>
</p>
@if (dso) {
<p>
selected collection: <b>{{getDspaceObjectName()}}</b>&nbsp;
<a href="javascript:void(0)" (click)="removeDspaceObject()">{{'admin.batch-import.page.remove' | translate}}</a>
</p>
}
<p>
<button class="btn btn-primary" (click)="this.selectCollection();">{{'admin.metadata-import.page.button.select-collection' | translate}}</button>
</p>
<div class="form-group">
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly">
<label class="form-check-label" for="validateOnly">
@@ -21,32 +23,35 @@
</div>
<ui-switch color="#ebebeb"
[checkedLabel]="'admin.metadata-import.page.toggle.upload' | translate"
[uncheckedLabel]="'admin.metadata-import.page.toggle.url' | translate"
[checked]="isUpload"
(change)="toggleUpload()" ></ui-switch>
<small class="form-text text-muted">
[checkedLabel]="'admin.metadata-import.page.toggle.upload' | translate"
[uncheckedLabel]="'admin.metadata-import.page.toggle.url' | translate"
[checked]="isUpload"
(change)="toggleUpload()" ></ui-switch>
<small class="form-text text-muted d-block">
{{'admin.batch-import.page.toggle.help' | translate}}
</small>
<ds-file-dropzone-no-uploader
*ngIf="isUpload"
data-test="file-dropzone"
(onFileAdded)="setFile($event)"
[dropMessageLabel]="'admin.batch-import.page.dropMsg'"
[dropMessageLabelReplacement]="'admin.batch-import.page.dropMsgReplace'">
</ds-file-dropzone-no-uploader>
@if (isUpload) {
<ds-file-dropzone-no-uploader
data-test="file-dropzone"
(onFileAdded)="setFile($event)"
[dropMessageLabel]="'admin.batch-import.page.dropMsg'"
[dropMessageLabelReplacement]="'admin.batch-import.page.dropMsgReplace'">
</ds-file-dropzone-no-uploader>
}
<div class="form-group mt-2" *ngIf="!isUpload">
<input class="form-control" type="text" placeholder="{{'admin.metadata-import.page.urlMsg' | translate}}"
data-test="file-url-input" [(ngModel)]="fileURL">
</div>
@if (!isUpload) {
<div class="mb-3 mt-2">
<input class="form-control" type="text" placeholder="{{'admin.metadata-import.page.urlMsg' | translate}}"
data-test="file-url-input" [(ngModel)]="fileURL">
</div>
}
<div class="space-children-mr">
<button class="btn btn-secondary" id="backButton"
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
<button class="btn btn-primary" id="proceedButton"
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
</div>
</div>

View File

@@ -1,7 +1,4 @@
import {
Location,
NgIf,
} from '@angular/common';
import { Location } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
@@ -36,7 +33,6 @@ import { FileDropzoneNoUploaderComponent } from '../../shared/upload/file-dropzo
selector: 'ds-batch-import-page',
templateUrl: './batch-import-page.component.html',
imports: [
NgIf,
TranslateModule,
FormsModule,
UiSwitchModule,

View File

@@ -1,7 +1,7 @@
<div class="container">
<h1 id="header">{{'admin.metadata-import.page.header' | translate}}</h1>
<p>{{'admin.metadata-import.page.help' | translate}}</p>
<div class="form-group">
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly">
<label class="form-check-label" for="validateOnly">

View File

@@ -4,66 +4,74 @@
<h1 class="flex-grow-1">{{ isNewService ? ('ldn-create-service.title' | translate) : ('ldn-edit-registered-service.title' | translate) }}</h1>
</div>
<!-- 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>
@if (!isNewService) {
<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>
</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>
[placeholder]="'ldn-new-service.form.placeholder.name' | translate" class="form-control"
formControlName="name"
id="name"
name="name"
type="text">
@if (formModel.get('name').invalid && formModel.get('name').touched) {
<div 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>
class="form-control" formControlName="description" id="description" name="description"></textarea>
</div>
<div class="mb-5 mt-5">
<!-- In the url section -->
<div class="d-flex align-items-center">
<div class="d-flex flex-column w-50 mr-2">
<div class="d-flex flex-column w-50 me-2">
<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>
[placeholder]="'ldn-new-service.form.placeholder.url' | translate" class="form-control"
formControlName="url"
id="url"
name="url"
type="text">
@if (formModel.get('url').invalid && formModel.get('url').touched) {
<div class="error-text">
{{ 'ldn-new-service.form.error.url' | translate }}
</div>
}
</div>
<div class="d-flex flex-column w-50">
<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>
[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">
@if (formModel.get('score').invalid && formModel.get('score').touched) {
<div class="error-text">
{{ 'ldn-new-service.form.error.score' | translate }}
</div>
}
</div>
</div>
</div>
@@ -73,21 +81,23 @@
<label for="lowerIp" class="font-weight-bold">{{ 'ldn-new-service.form.label.ip-range' | translate }}</label>
<div class="d-flex">
<input [class.invalid-field]="formModel.get('lowerIp').invalid && formModel.get('lowerIp').touched"
[placeholder]="'ldn-new-service.form.placeholder.lowerIp' | translate" class="form-control mr-2"
formControlName="lowerIp"
id="lowerIp"
name="lowerIp"
type="text">
[placeholder]="'ldn-new-service.form.placeholder.lowerIp' | translate" class="form-control me-2"
formControlName="lowerIp"
id="lowerIp"
name="lowerIp"
type="text">
<input [class.invalid-field]="formModel.get('upperIp').invalid && formModel.get('upperIp').touched"
[placeholder]="'ldn-new-service.form.placeholder.upperIp' | translate" class="form-control"
formControlName="upperIp"
id="upperIp"
name="upperIp"
type="text">
</div>
<div *ngIf="(formModel.get('lowerIp').invalid && formModel.get('lowerIp').touched) || (formModel.get('upperIp').invalid && formModel.get('upperIp').touched)" class="error-text">
{{ 'ldn-new-service.form.error.ipRange' | translate }}
[placeholder]="'ldn-new-service.form.placeholder.upperIp' | translate" class="form-control"
formControlName="upperIp"
id="upperIp"
name="upperIp"
type="text">
</div>
@if ((formModel.get('lowerIp').invalid && formModel.get('lowerIp').touched) || (formModel.get('upperIp').invalid && formModel.get('upperIp').touched)) {
<div class="error-text">
{{ 'ldn-new-service.form.error.ipRange' | translate }}
</div>
}
<div class="text-muted">
{{ 'ldn-new-service.form.hint.ipRange' | translate }}
</div>
@@ -97,223 +107,258 @@
<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" >
<div *ngIf="formModel.get('ldnUrl').errors['required']" class="error-text">
{{ 'ldn-new-service.form.error.ldnurl' | translate }}
[placeholder]="'ldn-new-service.form.placeholder.ldnUrl' | translate" class="form-control"
formControlName="ldnUrl"
id="ldnUrl"
name="ldnUrl"
type="text">
@if (formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched) {
<div >
@if (formModel.get('ldnUrl').errors['required']) {
<div class="error-text">
{{ 'ldn-new-service.form.error.ldnurl' | translate }}
</div>
}
@if (formModel.get('ldnUrl').errors['ldnUrlAlreadyAssociated']) {
<div class="error-text">
{{ 'ldn-new-service.form.error.ldnurl.ldnUrlAlreadyAssociated' | translate }}
</div>
}
</div>
<div *ngIf="formModel.get('ldnUrl').errors['ldnUrlAlreadyAssociated']" class="error-text">
{{ 'ldn-new-service.form.error.ldnurl.ldnUrlAlreadyAssociated' | translate }}
}
</div>
<!-- In the usesActorEmailId section -->
<div class="mb-5 mt-5">
<label class="status-label font-weight-bold" for="usesActorEmailId">{{ 'ldn-service-usesActorEmailId' | translate }}</label>
<div>
<input formControlName="usesActorEmailId" hidden id="usesActorEmailId"
name="usesActorEmailId" type="checkbox">
<div (click)="toggleUsesActorEmailId()"
[class.checked]="formModel.get('usesActorEmailId').value" class="toggle-switch">
<div class="slider"></div>
</div>
<div class="text-muted">
{{ 'ldn-service-usesActorEmailId-description' | translate }}
</div>
</div>
</div>
<!-- In the Inbound Patterns Labels section -->
<div class="row mb-1 mt-5" *ngIf="areControlsInitialized">
<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">
@if (areControlsInitialized) {
<div class="row mb-1 mt-5">
<div class="col">
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.inboundPattern' | translate }} </label>
</div>
<div class="col-sm-1">
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.automatic' | translate }}</label>
@if (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>
}
<div class="col-sm-2">
</div>
</ng-container>
<div class="col-sm-2">
</div>
</div>
}
<!-- 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">
<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" aria-expanded="false" aria-controls="inboundPatternDropdownButton">
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
ngbDropdownToggle></i>
<input
(click)="inboundPatternDropdown.open();"
[readonly]="true"
[value]="selectedInboundPatterns"
class="form-control w-80 scrollable-dropdown-input"
formControlName="patternLabel"
id="inboundPatternDropdownButton"
ngbDropdownAnchor
type="text"
[attr.aria-label]="'ldn-service-input-inbound-pattern-dropdown' | translate"
/>
<div aria-labelledby="inboundPatternDropdownButton"
class="dropdown-menu dropdown-menu-top w-100 "
ngbDropdownMenu>
<div class="scrollable-menu" role="listbox">
<button (click)="selectInboundPattern(pattern, i); $event.stopPropagation()"
*ngFor="let pattern of inboundPatterns; let internalIndex = index"
@if (areControlsInitialized) {
<div>
@for (patternGroup of formModel.get('notifyServiceInboundPatterns')['controls']; track patternGroup; let i = $index) {
<div
[class.marked-for-deletion]="markedForDeletionInboundPattern.includes(i)"
formGroupName="notifyServiceInboundPatterns">
<ng-container [formGroupName]="i">
<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" aria-expanded="false" aria-controls="inboundPatternDropdownButton">
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
ngbDropdownToggle></i>
<input
(click)="inboundPatternDropdown.open();"
[readonly]="true"
[value]="selectedInboundPatterns"
class="form-control w-80 scrollable-dropdown-input"
formControlName="patternLabel"
id="inboundPatternDropdownButton"
ngbDropdownAnchor
type="text"
[attr.aria-label]="'ldn-service-input-inbound-pattern-dropdown' | translate"
/>
<div aria-labelledby="inboundPatternDropdownButton"
class="dropdown-menu dropdown-menu-top w-100 "
ngbDropdownMenu>
<div class="scrollable-menu" role="listbox">
@for (pattern of inboundPatterns; track pattern; let internalIndex = $index) {
<button (click)="selectInboundPattern(pattern, i); $event.stopPropagation()"
[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>
</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="top-start">
<div class="position-relative right-addon" aria-expanded="false" aria-controls="inboundItemfilterDropdown" role="combobox">
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
ngbDropdownToggle></i>
<input
[readonly]="true"
class="form-control d-none w-100 scrollable-dropdown-input"
formControlName="constraint"
id="inboundItemfilterDropdown"
ngbDropdownAnchor
type="text"
[attr.aria-label]="'ldn-service-input-inbound-item-filter-dropdown' | translate"
/>
<input
(click)="inboundItemfilterDropdown.open();"
[readonly]="true"
class="form-control w-100 scrollable-dropdown-input"
formControlName="constraintFormatted"
id="inboundItemfilterDropdownPrettified"
ngbDropdownAnchor
type="text"
[attr.aria-label]="'ldn-service-input-inbound-item-filter-dropdown' | translate"
/>
<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 + '.label' | translate }}</div>
</button>
<div>{{ 'ldn-service.form.pattern.' + pattern + '.label' | translate }}</div>
</button>
}
</div>
</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"
[title]="'ldn-service-button-mark-inbound-deletion' | translate"
type="button">
<i class="fas fa-trash"></i>
</button>
<button (click)="unmarkForInboundPatternDeletion(i)"
*ngIf="markedForDeletionInboundPattern.includes(i)"
<div class="col">
@if (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" aria-expanded="false" aria-controls="inboundItemfilterDropdown" role="combobox">
<i aria-hidden="true" class="position-absolute scrollable-dropdown-toggle"
ngbDropdownToggle></i>
<input
[readonly]="true"
class="form-control d-none w-100 scrollable-dropdown-input"
formControlName="constraint"
id="inboundItemfilterDropdown"
ngbDropdownAnchor
type="text"
[attr.aria-label]="'ldn-service-input-inbound-item-filter-dropdown' | translate"
/>
<input
(click)="inboundItemfilterDropdown.open();"
[readonly]="true"
class="form-control w-100 scrollable-dropdown-input"
formControlName="constraintFormatted"
id="inboundItemfilterDropdownPrettified"
ngbDropdownAnchor
type="text"
[attr.aria-label]="'ldn-service-input-inbound-item-filter-dropdown' | translate"
/>
<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>
@for (constraint of (itemFiltersRD$ | async)?.payload?.page; track constraint; let internalIndex = $index) {
<button (click)="selectInboundItemFilter(constraint.id, i); $event.stopPropagation()"
class="dropdown-item collection-item text-truncate w-100"
ngbDropdownItem
type="button">
<div>{{ constraint.id + '.label' | translate }}</div>
</button>
}
</div>
</div>
</div>
</div>
}
</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"
[title]="'ldn-service-button-mark-inbound-deletion' | translate"
type="button">
<i class="fas fa-trash"></i>
</button>
@if (markedForDeletionInboundPattern.includes(i)) {
<button (click)="unmarkForInboundPatternDeletion(i)"
[title]="'ldn-service-button-unmark-inbound-deletion' | translate"
class="btn btn-warning "
type="button">
<i class="fas fa-undo"></i>
</button>
<i class="fas fa-undo"></i>
</button>
}
</div>
</div>
</div>
</div>
</ng-container>
</div>
</ng-container>
}
</div>
</div>
}
<span (click)="addInboundPattern()"
class="add-pattern-link mb-2">{{ 'ldn-new-service.form.label.addPattern' | translate }}</span>
class="add-pattern-link mb-2">{{ 'ldn-new-service.form.label.addPattern' | translate }}</span>
<hr>
<div class="form-group row">
<div class="col text-right space-children-mr">
<ng-content select="[before]"></ng-content>
<button (click)="resetFormAndLeave()" class="btn btn-outline-secondary" type="button">
<span>&nbsp;{{ 'submission.general.back.submit' | translate }}</span>
</button>
<button class="btn btn-primary" type="submit">
<span><i class="fas fa-save"></i>&nbsp;{{ 'ldn-new-service.form.label.submit' | translate }}</span>
</button>
<div class="form-group row">
<div class="col text-right space-children-mr">
<ng-content select="[before]"></ng-content>
<button (click)="resetFormAndLeave()" class="btn btn-outline-secondary" type="button">
<span>&nbsp;{{ 'submission.general.back.submit' | translate }}</span>
</button>
<button class="btn btn-primary" type="submit">
<span><i class="fas fa-save"></i>&nbsp;{{ 'ldn-new-service.form.label.submit' | translate }}</span>
</button>
</div>
</div>
</div>
</form>
</div>
<ng-template #confirmModal>
</form>
</div>
<ng-template #confirmModal>
<div class="modal-header">
<h4 *ngIf="!isNewService">{{'service.overview.edit.modal' | translate }}</h4>
<h4 *ngIf="isNewService">{{'service.overview.create.modal' | translate }}</h4>
@if (!isNewService) {
<h4>{{'service.overview.edit.modal' | translate }}</h4>
}
@if (isNewService) {
<h4>{{'service.overview.create.modal' | translate }}</h4>
}
<button (click)="closeModal()" aria-label="Close"
class="close" type="button">
class="close" type="button">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div *ngIf="!isNewService">
{{ 'service.overview.edit.body' | translate }}
</div>
<span *ngIf="isNewService">
{{ 'service.overview.create.body' | translate }}
</span>
</div>
<div class="modal-footer">
<div *ngIf="!isNewService">
<button (click)="closeModal()" class="btn btn-outline-secondary 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>
@if (!isNewService) {
<div>
{{ 'service.overview.edit.body' | translate }}
</div>
<div *ngIf="isNewService">
<button (click)="closeModal()" class="btn btn-outline-secondary mr-2 "
id="delete-confirm-new">{{ 'service.refuse.create' | translate }}
}
@if (isNewService) {
<span>
{{ 'service.overview.create.body' | translate }}
</span>
}
</div>
<div class="modal-footer">
@if (!isNewService) {
<div>
<button (click)="closeModal()" class="btn btn-outline-secondary me-2"
id="delete-confirm-edit">{{ 'service.detail.return' | translate }}
</button>
@if (!isNewService) {
<button (click)="patchService()"
class="btn btn-primary">{{ 'service.detail.update' | translate }}
</button>
}
</div>
}
@if (isNewService) {
<div>
<button (click)="closeModal()" class="btn btn-outline-secondary me-2 "
id="delete-confirm-new">{{ 'service.refuse.create' | translate }}
</button>
<button (click)="createService()"
class="btn btn-primary">{{ 'service.confirm.create' | translate }}
class="btn btn-primary">{{ 'service.confirm.create' | translate }}
</button>
</div>
</div>
</ng-template>
}
</div>
</ng-template>

View File

@@ -5,11 +5,7 @@ import {
transition,
trigger,
} from '@angular/animations';
import {
AsyncPipe,
NgForOf,
NgIf,
} from '@angular/common';
import { AsyncPipe } from '@angular/common';
import {
ChangeDetectorRef,
Component,
@@ -77,9 +73,7 @@ import { notifyPatterns } from '../ldn-services-patterns/ldn-service-coar-patter
imports: [
ReactiveFormsModule,
TranslateModule,
NgIf,
NgbDropdownModule,
NgForOf,
AsyncPipe,
],
})
@@ -131,6 +125,7 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
score: ['', [Validators.required, Validators.pattern('^0*(\.[0-9]+)?$|^1(\.0+)?$')]], inboundPattern: [''],
constraintPattern: [''],
enabled: [''],
usesActorEmailId: [''],
type: LDN_SERVICE.value,
});
}
@@ -184,7 +179,8 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
return rest;
});
const values = { ...this.formModel.value, enabled: true };
const values = { ...this.formModel.value, enabled: true,
usesActorEmailId: this.formModel.get('usesActorEmailId').value };
const ldnServiceData = this.ldnServicesService.create(values);
@@ -243,6 +239,7 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
ldnUrl: this.ldnService.ldnUrl,
type: this.ldnService.type,
enabled: this.ldnService.enabled,
usesActorEmailId: this.ldnService.usesActorEmailId,
lowerIp: this.ldnService.lowerIp,
upperIp: this.ldnService.upperIp,
});
@@ -390,6 +387,32 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy {
);
}
/**
* Toggles the usesActorEmailId field of the LDN service by sending a patch request
*/
toggleUsesActorEmailId() {
const newStatus = !this.formModel.get('usesActorEmailId').value;
if (!this.isNewService) {
const patchOperation: Operation = {
op: 'replace',
path: '/usesActorEmailId',
value: newStatus,
};
this.ldnServicesService.patch(this.ldnService, [patchOperation]).pipe(
getFirstCompletedRemoteData(),
).subscribe(
() => {
this.formModel.get('usesActorEmailId').setValue(newStatus);
this.cdRef.detectChanges();
},
);
} else {
this.formModel.get('usesActorEmailId').setValue(newStatus);
this.cdRef.detectChanges();
}
}
/**
* Closes the modal
*/

View File

@@ -12,6 +12,7 @@ import { LdnService } from '../ldn-services-model/ldn-services.model';
export const mockLdnService: LdnService = {
uuid: '1',
enabled: false,
usesActorEmailId: false,
score: 0,
id: 1,
lowerIp: '192.0.2.146',
@@ -49,6 +50,7 @@ export const mockLdnServiceRD$ = createSuccessfulRemoteDataObject$(mockLdnServic
export const mockLdnServices: LdnService[] = [{
uuid: '1',
enabled: false,
usesActorEmailId: false,
score: 0,
id: 1,
lowerIp: '192.0.2.146',
@@ -81,6 +83,7 @@ export const mockLdnServices: LdnService[] = [{
}, {
uuid: '2',
enabled: false,
usesActorEmailId: false,
score: 0,
id: 2,
lowerIp: '192.0.2.146',

View File

@@ -4,63 +4,67 @@
</div>
<div class="d-flex justify-content-end">
<button class="btn btn-success" routerLink="/admin/ldn/services/new"><i
class="fas fa-plus pr-2"></i>{{ 'process.overview.new' | translate }}</button>
class="fas fa-plus pe-2"></i>{{ 'process.overview.new' | translate }}</button>
</div>
<ds-pagination *ngIf="(ldnServicesRD$ | async)?.payload?.totalElements > 0"
[collectionSize]="(ldnServicesRD$ | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
[paginationOptions]="pageConfig">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">{{ 'service.overview.table.name' | translate }}</th>
<th scope="col">{{ 'service.overview.table.description' | translate }}</th>
<th scope="col">{{ 'service.overview.table.status' | translate }}</th>
<th scope="col">{{ 'service.overview.table.actions' | translate }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let ldnService of (ldnServicesRD$ | async)?.payload?.page">
<td class="col-3">{{ ldnService.name }}</td>
<td>
<ds-truncatable [id]="ldnService.id">
<ds-truncatable-part [id]="ldnService.id" [minLines]="2">
<div>
{{ ldnService.description }}
</div>
</ds-truncatable-part>
</ds-truncatable>
</td>
<td>
<span (click)="toggleStatus(ldnService, ldnServicesService)"
[ngClass]="{ 'status-enabled': ldnService.enabled, 'status-disabled': !ldnService.enabled }"
[title]="ldnService.enabled ? ('ldn-service.overview.table.clickToDisable' | translate) : ('ldn-service.overview.table.clickToEnable' | translate)"
class="status-indicator">
{{ ldnService.enabled ? ('ldn-service.overview.table.enabled' | translate) : ('ldn-service.overview.table.disabled' | translate) }}
</span>
</td>
<td>
<div class="btn-group">
<button
(click)="selectServiceToDelete(ldnService.id)"
[attr.aria-label]="'ldn-service-overview-select-delete' | translate"
class="btn btn-outline-danger">
<i class="fas fa-trash"></i>
</button>
<button [routerLink]="['/admin/ldn/services/edit/', ldnService.id]"
@if ((ldnServicesRD$ | async)?.payload?.totalElements > 0) {
<ds-pagination
[collectionSize]="(ldnServicesRD$ | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
[paginationOptions]="pageConfig">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th scope="col">{{ 'service.overview.table.name' | translate }}</th>
<th scope="col">{{ 'service.overview.table.description' | translate }}</th>
<th scope="col">{{ 'service.overview.table.status' | translate }}</th>
<th scope="col">{{ 'service.overview.table.actions' | translate }}</th>
</tr>
</thead>
<tbody>
@for (ldnService of (ldnServicesRD$ | async)?.payload?.page; track ldnService) {
<tr>
<td class="col-3">{{ ldnService.name }}</td>
<td>
<ds-truncatable [id]="ldnService.id">
<ds-truncatable-part [id]="ldnService.id" [minLines]="2">
<div>
{{ ldnService.description }}
</div>
</ds-truncatable-part>
</ds-truncatable>
</td>
<td>
<span (click)="toggleStatus(ldnService, ldnServicesService)"
[ngClass]="{ 'status-enabled': ldnService.enabled, 'status-disabled': !ldnService.enabled }"
[title]="ldnService.enabled ? ('ldn-service.overview.table.clickToDisable' | translate) : ('ldn-service.overview.table.clickToEnable' | translate)"
class="status-indicator">
{{ ldnService.enabled ? ('ldn-service.overview.table.enabled' | translate) : ('ldn-service.overview.table.disabled' | translate) }}
</span>
</td>
<td>
<div class="btn-group">
<button
(click)="selectServiceToDelete(ldnService.id)"
[attr.aria-label]="'ldn-service-overview-select-delete' | translate"
class="btn btn-outline-danger">
<i class="fas fa-trash"></i>
</button>
<button [routerLink]="['/admin/ldn/services/edit/', ldnService.id]"
[attr.aria-label]="'ldn-service-overview-select-edit' | translate"
class="btn btn-outline-dark">
<i class="fas fa-edit"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</ds-pagination>
<i class="fas fa-edit"></i>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</ds-pagination>
}
</div>
<ng-template #deleteModal>
@@ -72,8 +76,8 @@
<h4>{{'service.overview.delete.header' | translate }}</h4>
</div>
<button (click)="closeModal()" aria-label="Close"
[attr.aria-label]="'ldn-service-overview-close-modal' | translate"
class="close" type="button">
[attr.aria-label]="'ldn-service-overview-close-modal' | translate"
class="close" type="button">
<span aria-hidden="true">×</span>
</button>
</div>
@@ -84,12 +88,12 @@
</div>
<div class="mt-4 text-right">
<button (click)="closeModal()"
[attr.aria-label]="'ldn-service-overview-close-modal' | translate"
class="btn btn-outline-secondary mr-2">{{ 'service.detail.delete.cancel' | translate }}</button>
[attr.aria-label]="'ldn-service-overview-close-modal' | translate"
class="btn btn-outline-secondary me-2">{{ 'service.detail.delete.cancel' | translate }}</button>
<button (click)="deleteSelected(this.selectedServiceId.toString(), ldnServicesService)"
class="btn btn-danger"
[attr.aria-label]="'ldn-service-overview-select-delete' | translate"
id="delete-confirm">{{ 'service.overview.delete' | translate }}
class="btn btn-danger"
[attr.aria-label]="'ldn-service-overview-select-delete' | translate"
id="delete-confirm">{{ 'service.overview.delete' | translate }}
</button>
</div>
</div>

View File

@@ -1,8 +1,6 @@
import {
AsyncPipe,
NgClass,
NgFor,
NgIf,
} from '@angular/common';
import {
ChangeDetectionStrategy,
@@ -54,8 +52,6 @@ import { LdnService } from '../ldn-services-model/ldn-services.model';
styleUrls: ['./ldn-services-directory.component.scss'],
changeDetection: ChangeDetectionStrategy.Default,
imports: [
NgIf,
NgFor,
TranslateModule,
AsyncPipe,
PaginationComponent,

View File

@@ -52,6 +52,9 @@ export class LdnService extends CacheableObject {
@autoserialize
enabled: boolean;
@autoserialize
usesActorEmailId: boolean;
@autoserialize
ldnUrl: string;

View File

@@ -1 +1 @@
<ds-publication-claim [source]="'openaire'"></ds-publication-claim>
<ds-suggestion-sources></ds-suggestion-sources>

View File

@@ -6,8 +6,9 @@ import {
waitForAsync,
} from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';
import { PublicationClaimComponent } from '../../../notifications/suggestion-targets/publication-claim/publication-claim.component';
import { SuggestionSourcesComponent } from '../../../notifications/suggestions/sources/suggestion-sources.component';
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page.component';
describe('AdminNotificationsPublicationClaimPageComponent', () => {
@@ -20,17 +21,10 @@ describe('AdminNotificationsPublicationClaimPageComponent', () => {
CommonModule,
TranslateModule.forRoot(),
AdminNotificationsPublicationClaimPageComponent,
],
providers: [
AdminNotificationsPublicationClaimPageComponent,
MockComponent(SuggestionSourcesComponent),
],
schemas: [NO_ERRORS_SCHEMA],
}).overrideComponent(AdminNotificationsPublicationClaimPageComponent, {
remove: {
imports: [PublicationClaimComponent],
},
})
.compileComponents();
}).compileComponents();
}));
beforeEach(() => {

View File

@@ -1,14 +1,12 @@
import { Component } from '@angular/core';
import { PublicationClaimComponent } from '../../../notifications/suggestion-targets/publication-claim/publication-claim.component';
import { SuggestionSourcesComponent } from '../../../notifications/suggestions/sources/suggestion-sources.component';
@Component({
selector: 'ds-admin-notifications-publication-claim-page',
templateUrl: './admin-notifications-publication-claim-page.component.html',
styleUrls: ['./admin-notifications-publication-claim-page.component.scss'],
imports: [
PublicationClaimComponent,
],
imports: [ SuggestionSourcesComponent ],
standalone: true,
})
export class AdminNotificationsPublicationClaimPageComponent {

View File

@@ -2,7 +2,8 @@ import { Route } from '@angular/router';
import { authenticatedGuard } from '../../core/auth/authenticated.guard';
import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { qualityAssuranceBreadcrumbResolver } from '../../core/breadcrumbs/quality-assurance-breadcrumb.resolver';
import { sourcesBreadcrumbResolver } from '../../core/breadcrumbs/sources-breadcrumb.resolver';
import { PublicationClaimComponent } from '../../notifications/suggestions/targets/publication-claim/publication-claim.component';
import { AdminNotificationsPublicationClaimPageResolver } from '../../quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service';
import { QualityAssuranceEventsPageComponent } from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.component';
import { qualityAssuranceEventsPageResolver } from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver';
@@ -33,13 +34,28 @@ export const ROUTES: Route[] = [
showBreadcrumbsFluid: false,
},
},
{
canActivate: [ authenticatedGuard ],
path: `${PUBLICATION_CLAIMS_PATH}/:sourceId`,
pathMatch: 'full',
component: PublicationClaimComponent,
resolve: {
breadcrumb: sourcesBreadcrumbResolver,
openaireQualityAssuranceEventsParams: AdminNotificationsPublicationClaimPageResolver,
},
data: {
title: 'admin.notifications.publicationclaim.page.title',
breadcrumbKey: 'admin.notifications.publicationclaim',
showBreadcrumbsFluid: false,
},
},
{
canActivate: [authenticatedGuard],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`,
component: QualityAssuranceTopicsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: qualityAssuranceBreadcrumbResolver,
breadcrumb: sourcesBreadcrumbResolver,
openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver,
},
data: {
@@ -85,7 +101,7 @@ export const ROUTES: Route[] = [
component: QualityAssuranceEventsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: qualityAssuranceBreadcrumbResolver,
breadcrumb: sourcesBreadcrumbResolver,
openaireQualityAssuranceEventsParams: qualityAssuranceEventsPageResolver,
},
data: {

View File

@@ -13,11 +13,13 @@
</li>
<li class="nav-item">
<a class="nav-link" [routerLink]="'outbound'" [queryParams]="{view: 'table'}">{{'admin.notify.dashboard.outbound-logs' | translate}}</a>
</ul>
<div class="mt-2">
<ds-admin-notify-metrics *ngIf="(notifyMetricsRows$ | async)?.length" [boxesConfig]="notifyMetricsRows$ | async"></ds-admin-notify-metrics>
</ul>
<div class="mt-2">
@if ((notifyMetricsRows$ | async)?.length) {
<ds-admin-notify-metrics [boxesConfig]="notifyMetricsRows$ | async"></ds-admin-notify-metrics>
}
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,7 +1,4 @@
import {
AsyncPipe,
NgIf,
} from '@angular/common';
import { AsyncPipe } from '@angular/common';
import {
Component,
Inject,
@@ -46,7 +43,6 @@ import {
imports: [
AdminNotifyMetricsComponent,
RouterLink,
NgIf,
TranslateModule,
AsyncPipe,
],

View File

@@ -5,12 +5,14 @@
</button>
</div>
<div class="modal-body p-4">
<div *ngFor="let key of notifyMessageKeys">
@for (key of notifyMessageKeys; track key) {
<div>
<div class="row mb-4">
<div class="font-weight-bold col">{{ key + '.notify-detail-modal' | translate}}</div>
<div class="col text-right">{{'notify-detail-modal.' + notifyMessage[key] | translate: {default: notifyMessage[key] ?? "n/a" } }}</div>
</div>
</div>
}
<div class="d-flex justify-content-end">
<button class="btn-primary" (click)="toggleCoarMessage()">
@@ -18,5 +20,7 @@
</button>
</div>
<pre @fadeIn [innerHTML]="notifyMessage.message" class="bg-secondary text-white mt-2 p-2" *ngIf="isCoarMessageVisible"></pre>
@if (isCoarMessageVisible) {
<pre @fadeIn [innerHTML]="notifyMessage.message" class="bg-secondary text-white mt-2 p-2"></pre>
}
</div>

View File

@@ -1,7 +1,4 @@
import {
NgForOf,
NgIf,
} from '@angular/common';
import {
Component,
EventEmitter,
@@ -26,9 +23,7 @@ import { AdminNotifyMessage } from '../models/admin-notify-message.model';
],
standalone: true,
imports: [
NgForOf,
TranslateModule,
NgIf,
],
})
/**

View File

@@ -3,10 +3,12 @@
<div class="col-12 col-md-3 text-left h4">{{((isInbound$ | async) ? 'admin.notify.dashboard.inbound' : 'admin.notify.dashboard.outbound') | translate}}</div>
<div class="col-md-9">
<div class="h4">
<button (click)="resetDefaultConfiguration()" *ngIf="(selectedSearchConfig$ | async) !== defaultConfiguration" class="badge badge-primary mr-1 mb-1">
{{ 'admin-notify-logs.' + (selectedSearchConfig$ | async) | translate}}
<span> ×</span>
</button>
@if ((selectedSearchConfig$ | async) !== defaultConfiguration) {
<button (click)="resetDefaultConfiguration()" class="badge bg-primary me-1 mb-1">
{{ 'admin-notify-logs.' + (selectedSearchConfig$ | async) | translate}}
<span> ×</span>
</button>
}
</div>
<ds-search-labels [inPlaceSearch]="true"></ds-search-labels>
</div>

View File

@@ -1,7 +1,4 @@
import {
AsyncPipe,
NgIf,
} from '@angular/common';
import { AsyncPipe } from '@angular/common';
import {
Component,
Inject,
@@ -39,7 +36,6 @@ import { ThemedSearchComponent } from '../../../../shared/search/themed-search.c
ThemedSearchComponent,
AsyncPipe,
TranslateModule,
NgIf,
],
})

View File

@@ -1,9 +1,13 @@
<div class="mb-5" *ngFor="let row of boxesConfig">
<div class="mb-2">{{ row.title | translate }}</div>
<div class="row justify-content-between">
<div class="col-sm" *ngFor="let box of row.boxes">
<ds-notification-box (selectedBoxConfig)="navigateToSelectedSearchConfig($event)" [boxConfig]="box"></ds-notification-box>
@for (row of boxesConfig; track row) {
<div class="mb-5">
<div class="mb-2">{{ row.title | translate }}</div>
<div class="row justify-content-between">
@for (box of row.boxes; track box) {
<div class="col-sm">
<ds-notification-box (selectedBoxConfig)="navigateToSelectedSearchConfig($event)" [boxConfig]="box"></ds-notification-box>
</div>
}
</div>
</div>
</div>
}

View File

@@ -1,4 +1,4 @@
import { NgForOf } from '@angular/common';
import {
Component,
Input,
@@ -17,7 +17,6 @@ import { AdminNotifyMetricsRow } from './admin-notify-metrics.model';
imports: [
NotificationBoxComponent,
TranslateModule,
NgForOf,
],
})
/**

View File

@@ -11,41 +11,57 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let message of (messagesSubject$ | async)">
<td class="text-nowrap">
<div *ngIf="message.queueLastStartTime">{{ message.queueLastStartTime | date:"YYYY/MM/d hh:mm:ss" }}</div>
<div *ngIf="!message.queueLastStartTime">n/a</div>
</td>
<td>
<ds-truncatable [id]="message.id">
<ds-truncatable-part [id]="message.id" [minLines]="2">
<a *ngIf="message.relatedItem" [routerLink]="'/items/' + (message.context || message.object)">{{ message.relatedItem }}</a>
</ds-truncatable-part>
</ds-truncatable>
<div *ngIf="!message.relatedItem">n/a</div>
</td>
<td>
<div *ngIf="message.ldnService">{{ message.ldnService }}</div>
<div *ngIf="!message.ldnService">n/a</div>
</td>
<td>
<div>{{ message.activityStreamType }}</div>
</td>
<td>
<div class="text-nowrap">{{ 'notify-detail-modal.' + message.queueStatusLabel | translate }}</div>
</td>
<td>
<div class="d-flex flex-column">
<button class="btn mb-2 btn-dark" (click)="openDetailModal(message)">{{ 'notify-message-result.detail' | translate }}</button>
<button *ngIf="message.queueStatusLabel !== reprocessStatus && validStatusesForReprocess.includes(message.queueStatusLabel)"
(click)="reprocessMessage(message)"
class="btn btn-warning"
>
{{ 'notify-message-result.reprocess' | translate }}
</button>
</div>
</td>
</tr>
@for (message of (messagesSubject$ | async); track message) {
<tr>
<td class="text-nowrap">
@if (message.queueLastStartTime) {
<div>{{ message.queueLastStartTime | date:"YYYY/MM/d hh:mm:ss" }}</div>
}
@if (!message.queueLastStartTime) {
<div>n/a</div>
}
</td>
<td>
<ds-truncatable [id]="message.id">
<ds-truncatable-part [id]="message.id" [minLines]="2">
@if (message.relatedItem) {
<a [routerLink]="'/items/' + (message.context || message.object)">{{ message.relatedItem }}</a>
}
</ds-truncatable-part>
</ds-truncatable>
@if (!message.relatedItem) {
<div>n/a</div>
}
</td>
<td>
@if (message.ldnService) {
<div>{{ message.ldnService }}</div>
}
@if (!message.ldnService) {
<div>n/a</div>
}
</td>
<td>
<div>{{ message.activityStreamType }}</div>
</td>
<td>
<div class="text-nowrap">{{ 'notify-detail-modal.' + message.queueStatusLabel | translate }}</div>
</td>
<td>
<div class="d-flex flex-column">
<button class="btn mb-2 btn-dark" (click)="openDetailModal(message)">{{ 'notify-message-result.detail' | translate }}</button>
@if (message.queueStatusLabel !== reprocessStatus && validStatusesForReprocess.includes(message.queueStatusLabel)) {
<button
(click)="reprocessMessage(message)"
class="btn btn-warning"
>
{{ 'notify-message-result.reprocess' | translate }}
</button>
}
</div>
</td>
</tr>
}
</tbody>
</table>
</div>

View File

@@ -1,8 +1,6 @@
import {
AsyncPipe,
DatePipe,
NgForOf,
NgIf,
} from '@angular/common';
import {
Component,
@@ -42,8 +40,6 @@ import { AdminNotifyMessagesService } from '../services/admin-notify-messages.se
standalone: true,
imports: [
TranslateModule,
NgForOf,
NgIf,
DatePipe,
AsyncPipe,
TruncatableComponent,

View File

@@ -8,51 +8,62 @@
<p id="create-new" class="mb-2"><a [routerLink]="'add'" class="btn btn-success">{{'admin.registries.bitstream-formats.create.new' | translate}}</a></p>
<ds-pagination
*ngIf="(bitstreamFormats$ | async)?.payload?.totalElements > 0"
[paginationOptions]="pageConfig"
[collectionSize]="(bitstreamFormats$ | async)?.payload?.totalElements"
[hideGear]="false"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="formats" class="table table-striped table-hover">
<thead>
<tr>
<th scope="col"><span class="sr-only">{{'admin.registries.bitstream-formats.table.selected' | translate}}</span></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>
<th scope="col">{{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let bitstreamFormat of (bitstreamFormats$ | async)?.payload?.page">
<td>
<label class="mb-0">
<input type="checkbox"
[attr.aria-label]="'admin.registries.bitstream-formats.select' | translate"
[checked]="(selectedBitstreamFormatIDs$ | async)?.includes(bitstreamFormat.id)"
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
>
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}&#125;</span>
</label>
</td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.id}}</a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.shortDescription}}</a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.table.internal' | translate}})</span></a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td>
</tr>
</tbody>
</table>
@if ((bitstreamFormats$ | async)?.payload?.totalElements > 0) {
<ds-pagination
[paginationOptions]="pageConfig"
[collectionSize]="(bitstreamFormats$ | async)?.payload?.totalElements"
[hideGear]="false"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="formats" class="table table-striped table-hover">
<thead>
<tr>
<th scope="col"><span class="sr-only">{{'admin.registries.bitstream-formats.table.selected' | translate}}</span></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>
<th scope="col">{{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}}</th>
</tr>
</thead>
<tbody>
@for (bitstreamFormat of (bitstreamFormats$ | async)?.payload?.page; track bitstreamFormat) {
<tr>
<td>
<label class="form-label mb-0">
<input type="checkbox"
[attr.aria-label]="'admin.registries.bitstream-formats.select' | translate"
[checked]="(selectedBitstreamFormatIDs$ | async)?.includes(bitstreamFormat.id)"
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
>
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}&#125;</span>
</label>
</td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.id}}</a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.shortDescription}}</a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.mimetype}} @if (bitstreamFormat.internal) {
<span>({{'admin.registries.bitstream-formats.table.internal' | translate}})</span>
}</a></td>
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td>
</tr>
}
</tbody>
</table>
</div>
</ds-pagination>
}
@if ((bitstreamFormats$ | async)?.payload?.totalElements === 0) {
<div class="alert alert-info" role="alert">
{{'admin.registries.bitstream-formats.no-items' | translate}}
</div>
</ds-pagination>
<div *ngIf="(bitstreamFormats$ | async)?.payload?.totalElements === 0" class="alert alert-info" role="alert">
{{'admin.registries.bitstream-formats.no-items' | translate}}
</div>
}
<div>
<button *ngIf="(bitstreamFormats$ | async)?.payload?.page?.length > 0" class="btn btn-primary deselect" (click)="deselectAll()">{{'admin.registries.bitstream-formats.table.deselect-all' | translate}}</button>
<button *ngIf="(bitstreamFormats$ | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFormats()">{{'admin.registries.bitstream-formats.table.delete' | translate}}</button>
@if ((bitstreamFormats$ | async)?.payload?.page?.length > 0) {
<button class="btn btn-primary deselect" (click)="deselectAll()">{{'admin.registries.bitstream-formats.table.deselect-all' | translate}}</button>
}
@if ((bitstreamFormats$ | async)?.payload?.page?.length > 0) {
<button type="submit" class="btn btn-danger float-end" (click)="deleteFormats()">{{'admin.registries.bitstream-formats.table.delete' | translate}}</button>
}
</div>
</div>
</div>

View File

@@ -1,8 +1,4 @@
import {
AsyncPipe,
NgForOf,
NgIf,
} from '@angular/common';
import { AsyncPipe } from '@angular/common';
import {
Component,
OnDestroy,
@@ -41,12 +37,10 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio
selector: 'ds-bitstream-formats',
templateUrl: './bitstream-formats.component.html',
imports: [
NgIf,
AsyncPipe,
RouterLink,
TranslateModule,
PaginationComponent,
NgForOf,
],
standalone: true,
})

View File

@@ -1,3 +1,5 @@
<ds-form *ngIf="formModel"
[formId]="'comcol-form-id'"
[formModel]="formModel" (submitForm)="onSubmit()" (cancel)="onCancel()"></ds-form>
@if (formModel) {
<ds-form
[formId]="'comcol-form-id'"
[formModel]="formModel" (submitForm)="onSubmit()" (cancel)="onCancel()"></ds-form>
}

View File

@@ -1,4 +1,4 @@
import { NgIf } from '@angular/common';
import {
Component,
EventEmitter,
@@ -35,7 +35,6 @@ import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-p
templateUrl: './format-form.component.html',
imports: [
FormComponent,
NgIf,
],
standalone: true,
})
@@ -64,7 +63,7 @@ export class FormatFormComponent implements OnInit {
*/
arrayElementLayout: DynamicFormControlLayout = {
grid: {
group: 'form-row',
group: 'row',
},
};

View File

@@ -1,60 +1,65 @@
<div class="container">
<div class="metadata-registry row">
<div class="col-12">
<div class="metadata-registry row">
<div class="col-12">
<h1 id="header" class="border-bottom pb-2">{{'admin.registries.metadata.head' | translate}}</h1>
<h1 id="header" class="border-bottom pb-2">{{'admin.registries.metadata.head' | translate}}</h1>
<p id="description" class="pb-2">{{'admin.registries.metadata.description' | translate}}</p>
<p id="description" class="pb-2">{{'admin.registries.metadata.description' | translate}}</p>
<ds-metadata-schema-form (submitForm)="forceUpdateSchemas()"></ds-metadata-schema-form>
<ds-metadata-schema-form (submitForm)="forceUpdateSchemas()"></ds-metadata-schema-form>
<ds-pagination
*ngIf="(metadataSchemas | async)?.payload?.totalElements > 0"
[paginationOptions]="config"
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="metadata-schemas" class="table table-striped table-hover">
<thead>
<tr>
<th scope="col"><span class="sr-only">{{'admin.registries.metadata.schemas.table.selected' | translate}}</span></th>
<th scope="col">{{'admin.registries.metadata.schemas.table.id' | translate}}</th>
<th scope="col">{{'admin.registries.metadata.schemas.table.namespace' | translate}}</th>
<th scope="col">{{'admin.registries.metadata.schemas.table.name' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
[ngClass]="{'table-primary' : (activeMetadataSchema$ | async)?.id === schema.id}">
<td>
<label class="mb-0">
<input type="checkbox"
[checked]="(selectedMetadataSchemaIDs$ | async)?.includes(schema.id)"
(change)="selectMetadataSchema(schema, $event)"
>
<span class="sr-only">{{(((selectedMetadataSchemaIDs$ | async)?.includes(schema.id)) ? 'admin.registries.metadata.schemas.deselect' : 'admin.registries.metadata.schemas.select') | translate}}</span>
</label>
</td>
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.id}}</a></td>
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.namespace}}</a></td>
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.prefix}}</a></td>
</tr>
</tbody>
</table>
</div>
</ds-pagination>
<div *ngIf="(metadataSchemas | async)?.payload?.totalElements === 0" class="alert alert-info w-100 mb-2" role="alert">
{{'admin.registries.metadata.schemas.no-items' | translate}}
</div>
<div>
<button *ngIf="(metadataSchemas | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteSchemas()">{{'admin.registries.metadata.schemas.table.delete' | translate}}</button>
</div>
@if ((metadataSchemas | async)?.payload?.totalElements > 0) {
<ds-pagination
[paginationOptions]="config"
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="metadata-schemas" class="table table-striped table-hover">
<thead>
<tr>
<th scope="col"><span class="sr-only">{{'admin.registries.metadata.schemas.table.selected' | translate}}</span></th>
<th scope="col">{{'admin.registries.metadata.schemas.table.id' | translate}}</th>
<th scope="col">{{'admin.registries.metadata.schemas.table.namespace' | translate}}</th>
<th scope="col">{{'admin.registries.metadata.schemas.table.name' | translate}}</th>
</tr>
</thead>
<tbody>
@for (schema of (metadataSchemas | async)?.payload?.page; track schema) {
<tr
[ngClass]="{'table-primary' : (activeMetadataSchema$ | async)?.id === schema.id}">
<td>
<label class="form-label mb-0">
<input type="checkbox"
[checked]="(selectedMetadataSchemaIDs$ | async)?.includes(schema.id)"
(change)="selectMetadataSchema(schema, $event)"
>
<span class="sr-only">{{(((selectedMetadataSchemaIDs$ | async)?.includes(schema.id)) ? 'admin.registries.metadata.schemas.deselect' : 'admin.registries.metadata.schemas.select') | translate}}</span>
</label>
</td>
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.id}}</a></td>
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.namespace}}</a></td>
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.prefix}}</a></td>
</tr>
}
</tbody>
</table>
</div>
</ds-pagination>
}
@if ((metadataSchemas | async)?.payload?.totalElements === 0) {
<div class="alert alert-info w-100 mb-2" role="alert">
{{'admin.registries.metadata.schemas.no-items' | translate}}
</div>
}
<div>
@if ((metadataSchemas | async)?.payload?.page?.length > 0) {
<button type="submit" class="btn btn-danger float-end" (click)="deleteSchemas()">{{'admin.registries.metadata.schemas.table.delete' | translate}}</button>
}
</div>
</div>
</div>
</div>

View File

@@ -1,8 +1,6 @@
import {
AsyncPipe,
NgClass,
NgForOf,
NgIf,
} from '@angular/common';
import {
Component,
@@ -49,8 +47,6 @@ import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-sch
TranslateModule,
AsyncPipe,
PaginationComponent,
NgIf,
NgForOf,
NgClass,
RouterLink,
],

View File

@@ -1,18 +1,16 @@
<div *ngIf="activeMetadataSchema$ | async; then editheader; else createHeader"></div>
@if (activeMetadataSchema$ | async) {
<h2>{{messagePrefix + '.edit' | translate}}</h2>
} @else {
<h2>{{messagePrefix + '.create' | translate}}</h2>
}
<ng-template #createHeader>
<h2>{{messagePrefix + '.create' | translate}}</h2>
</ng-template>
<ng-template #editheader>
<h2>{{messagePrefix + '.edit' | translate}}</h2>
</ng-template>
<ds-form [formId]="formId"
[formModel]="formModel"
[formGroup]="formGroup"
[formLayout]="formLayout"
(cancel)="onCancel()"
(submitForm)="onSubmit()">
[formModel]="formModel"
[formGroup]="formGroup"
[formLayout]="formLayout"
(cancel)="onCancel()"
(submitForm)="onSubmit()">
</ds-form>

View File

@@ -1,7 +1,4 @@
import {
AsyncPipe,
NgIf,
} from '@angular/common';
import { AsyncPipe } from '@angular/common';
import {
Component,
EventEmitter,
@@ -39,7 +36,6 @@ import { FormComponent } from '../../../../shared/form/form.component';
selector: 'ds-metadata-schema-form',
templateUrl: './metadata-schema-form.component.html',
imports: [
NgIf,
AsyncPipe,
TranslateModule,
FormComponent,

View File

@@ -1,18 +1,16 @@
<div *ngIf="registryService.getActiveMetadataField() | async; then editheader; else createHeader"></div>
@if (registryService.getActiveMetadataField() | async) {
<h2>{{messagePrefix + '.edit' | translate}}</h2>
} @else {
<h2>{{messagePrefix + '.create' | translate}}</h2>
}
<ng-template #createHeader>
<h2>{{messagePrefix + '.create' | translate}}</h2>
</ng-template>
<ng-template #editheader>
<h2>{{messagePrefix + '.edit' | translate}}</h2>
</ng-template>
<ds-form [formId]="formId"
[formModel]="formModel"
[formLayout]="formLayout"
[formGroup]="formGroup"
(cancel)="onCancel()"
(submit)="onSubmit()">
[formModel]="formModel"
[formLayout]="formLayout"
[formGroup]="formGroup"
(cancel)="onCancel()"
(submit)="onSubmit()">
</ds-form>

View File

@@ -1,7 +1,4 @@
import {
AsyncPipe,
NgIf,
} from '@angular/common';
import { AsyncPipe } from '@angular/common';
import {
Component,
EventEmitter,
@@ -35,7 +32,6 @@ import { FormComponent } from '../../../../shared/form/form.component';
selector: 'ds-metadata-field-form',
templateUrl: './metadata-field-form.component.html',
imports: [
NgIf,
FormComponent,
TranslateModule,
AsyncPipe,

View File

@@ -8,52 +8,59 @@
<ds-metadata-field-form
[metadataSchema]="schema"
(submitForm)="forceUpdateFields()"></ds-metadata-field-form>
(submitForm)="forceUpdateFields()"></ds-metadata-field-form>
<h2>{{'admin.registries.schema.fields.head' | translate}}</h2>
<ng-container *ngVar="(metadataFields$ | async)?.payload as fields">
<ds-pagination
*ngIf="fields?.totalElements > 0"
[paginationOptions]="config"
[collectionSize]="fields?.totalElements"
[hideGear]="false"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="metadata-fields" class="table table-striped table-hover">
<thead>
<tr>
<th><span class="sr-only">{{'admin.registries.schema.fields.table.selected' | translate}}</span></th>
<th scope="col">{{'admin.registries.schema.fields.table.id' | translate}}</th>
<th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th>
<th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let field of fields?.page"
[ngClass]="{'table-primary' : (activeField$ | async)?.id === field.id}">
<td *ngVar="(selectedMetadataFieldIDs$ | async)?.includes(field.id) as selected">
<input type="checkbox"
[attr.aria-label]="(selected ? 'admin.registries.schema.fields.deselect' : 'admin.registries.schema.fields.select') | translate"
[checked]="selected"
(change)="selectMetadataField(field, $event)">
</td>
<td class="selectable-row" (click)="editField(field)">{{field.id}}</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>
</table>
</div>
</ds-pagination>
@if (fields?.totalElements > 0) {
<ds-pagination
[paginationOptions]="config"
[collectionSize]="fields?.totalElements"
[hideGear]="false"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="metadata-fields" class="table table-striped table-hover">
<thead>
<tr>
<th><span class="sr-only">{{'admin.registries.schema.fields.table.selected' | translate}}</span></th>
<th scope="col">{{'admin.registries.schema.fields.table.id' | translate}}</th>
<th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th>
<th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th>
</tr>
</thead>
<tbody>
@for (field of fields?.page; track field) {
<tr
[ngClass]="{'table-primary' : (activeField$ | async)?.id === field.id}">
<td *ngVar="(selectedMetadataFieldIDs$ | async)?.includes(field.id) as selected">
<input type="checkbox"
[attr.aria-label]="(selected ? 'admin.registries.schema.fields.deselect' : 'admin.registries.schema.fields.select') | translate"
[checked]="selected"
(change)="selectMetadataField(field, $event)">
</td>
<td class="selectable-row" (click)="editField(field)">{{field.id}}</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>
</table>
</div>
</ds-pagination>
}
<div *ngIf="fields?.totalElements === 0" class="alert alert-info w-100 mb-2" role="alert">
{{'admin.registries.schema.fields.no-items' | translate}}
</div>
@if (fields?.totalElements === 0) {
<div class="alert alert-info w-100 mb-2" role="alert">
{{'admin.registries.schema.fields.no-items' | translate}}
</div>
}
<div>
<button [routerLink]="['/admin/registries/metadata']" class="btn btn-primary">{{'admin.registries.schema.return' | translate}}</button>
<button *ngIf="fields?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFields()">{{'admin.registries.schema.fields.table.delete' | translate}}</button>
@if (fields?.page?.length > 0) {
<button type="submit" class="btn btn-danger float-end" (click)="deleteFields()">{{'admin.registries.schema.fields.table.delete' | translate}}</button>
}
</div>
</ng-container>

View File

@@ -1,8 +1,6 @@
import {
AsyncPipe,
NgClass,
NgForOf,
NgIf,
} from '@angular/common';
import {
Component,
@@ -59,8 +57,6 @@ import { MetadataFieldFormComponent } from './metadata-field-form/metadata-field
MetadataFieldFormComponent,
TranslateModule,
PaginationComponent,
NgIf,
NgForOf,
NgClass,
RouterLink,
],

View File

@@ -1,64 +1,72 @@
<div class="container">
<div class="metadata-registry row">
<div class="col-12">
<div class="metadata-registry row">
<div class="col-12">
<h1 id="header" class="border-bottom pb-2">{{ "admin.reports.collections.head" | translate }}</h1>
<h1 id="header" class="border-bottom pb-2">{{ "admin.reports.collections.head" | translate }}</h1>
<div id="metadatadiv">
<ngb-accordion [closeOthers]="true" activeIds="filters" #acc="ngbAccordion">
<ngb-panel id="filters">
<ng-template ngbPanelTitle>
{{ "admin.reports.commons.filters" | translate }}
</ng-template>
<ng-template ngbPanelContent>
<div class="container">
<div class="row">
<span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{ "admin.reports.button.show-collections" | translate }}</button>
</div>
<ds-filters [filtersForm]="filtersFormGroup()"></ds-filters>
<div class="row">
<span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{ "admin.reports.button.show-collections" | translate }}</button>
</div>
</div>
</ng-template>
</ngb-panel>
<ngb-panel id="collections">
<ng-template ngbPanelTitle>
{{ "admin.reports.collections.collections-report" | translate }}
</ng-template>
<ng-template ngbPanelContent>
<table id="table" class="table table-striped">
<thead>
<tr class="header">
<th rowspan="2">{{ "admin.reports.collections.community" | translate }}</th>
<th rowspan="2">{{ "admin.reports.collections.collection" | translate }}</th>
<th>{{ "admin.reports.collections.nb_items" | translate }}</th>
<th>{{ "admin.reports.collections.match_all_selected_filters" | translate }}</th>
<th *ngFor="let filter of results.summary.values | keyvalue">{{ ("admin.reports.commons.filters." + getGroup(filter.key) + "." + filter.key) | translate }}</th>
</tr>
<tr class="header">
<th class="num">{{ results.summary.nbTotalItems }}</th>
<th class="num">{{ results.summary.allFiltersValue }}</th>
<th class="num" *ngFor="let filter of results.summary.values | keyvalue">{{ filter.value }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let coll of results.collections">
<td><a href="/handle/{{ coll.communityHandle }}" rel="noopener noreferrer" target="_blank">{{ coll.communityLabel }}</a></td>
<td><a href="/handle/{{ coll.handle }}" rel="noopener noreferrer" target="_blank">{{ coll.label }}</a></td>
<td class="num">{{ coll.nbTotalItems }}</td>
<td class="num">{{ coll.allFiltersValue }}</td>
<td class="num" *ngFor="let filter of results.summary.values | keyvalue">{{ coll.values[filter.key] || 0 }}</td>
</tr>
</tbody>
</table>
</ng-template>
</ngb-panel>
</ngb-accordion>
</div>
<div id="metadatadiv">
<ngb-accordion [closeOthers]="true" activeIds="filters" #acc="ngbAccordion">
<ngb-panel id="filters">
<ng-template ngbPanelTitle>
{{ "admin.reports.commons.filters" | translate }}
</ng-template>
<ng-template ngbPanelContent>
<div class="container">
<div class="row">
<span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{ "admin.reports.button.show-collections" | translate }}</button>
</div>
<ds-filters [filtersForm]="filtersFormGroup()"></ds-filters>
<div class="row">
<span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{ "admin.reports.button.show-collections" | translate }}</button>
</div>
</div>
</ng-template>
</ngb-panel>
<ngb-panel id="collections">
<ng-template ngbPanelTitle>
{{ "admin.reports.collections.collections-report" | translate }}
</ng-template>
<ng-template ngbPanelContent>
<table id="table" class="table table-striped">
<thead>
<tr class="header">
<th rowspan="2">{{ "admin.reports.collections.community" | translate }}</th>
<th rowspan="2">{{ "admin.reports.collections.collection" | translate }}</th>
<th>{{ "admin.reports.collections.nb_items" | translate }}</th>
<th>{{ "admin.reports.collections.match_all_selected_filters" | translate }}</th>
@for (filter of results.summary.values | keyvalue; track filter) {
<th>{{ ("admin.reports.commons.filters." + getGroup(filter.key) + "." + filter.key) | translate }}</th>
}
</tr>
<tr class="header">
<th class="num">{{ results.summary.nbTotalItems }}</th>
<th class="num">{{ results.summary.allFiltersValue }}</th>
@for (filter of results.summary.values | keyvalue; track filter) {
<th class="num">{{ filter.value }}</th>
}
</tr>
</thead>
<tbody>
@for (coll of results.collections; track coll) {
<tr>
<td><a href="/handle/{{ coll.communityHandle }}" rel="noopener noreferrer" target="_blank">{{ coll.communityLabel }}</a></td>
<td><a href="/handle/{{ coll.handle }}" rel="noopener noreferrer" target="_blank">{{ coll.label }}</a></td>
<td class="num">{{ coll.nbTotalItems }}</td>
<td class="num">{{ coll.allFiltersValue }}</td>
@for (filter of results.summary.values | keyvalue; track filter) {
<td class="num">{{ coll.values[filter.key] || 0 }}</td>
}
</tr>
}
</tbody>
</table>
</ng-template>
</ngb-panel>
</ngb-accordion>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,4 +1,8 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import {
provideHttpClient,
withInterceptorsFromDi,
} from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
ComponentFixture,
@@ -39,22 +43,21 @@ describe('FiltersComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
NgbAccordionModule,
schemas: [NO_ERRORS_SCHEMA],
imports: [NgbAccordionModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock,
},
}),
HttpClientTestingModule,
FilteredCollectionsComponent,
],
FilteredCollectionsComponent],
providers: [
FormBuilder,
DspaceRestService,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
],
schemas: [NO_ERRORS_SCHEMA],
});
}));

View File

@@ -1,7 +1,4 @@
import {
KeyValuePipe,
NgForOf,
} from '@angular/common';
import { KeyValuePipe } from '@angular/common';
import {
Component,
OnInit,
@@ -37,7 +34,6 @@ import { FilteredCollections } from './filtered-collections.model';
NgbAccordionModule,
FiltersComponent,
KeyValuePipe,
NgForOf,
],
standalone: true,
})

View File

@@ -0,0 +1,8 @@
@if (shouldShowButton$ | async) {
<button class="export-button btn btn-dark btn-sm"
[ngbTooltip]="tooltipMsg | translate"
(click)="export()"
[title]="tooltipMsg | translate" [attr.aria-label]="tooltipMsg | translate">
<i class="fas fa-file-export fa-fw"></i>
</button>
}

View File

@@ -0,0 +1,4 @@
.export-button {
background: var(--ds-admin-sidebar-bg);
border-color: var(--ds-admin-sidebar-bg);
}

View File

@@ -0,0 +1,194 @@
import {
ComponentFixture,
TestBed,
waitForAsync,
} from '@angular/core/testing';
import {
FormControl,
FormGroup,
} from '@angular/forms';
import { By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs';
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
import { ScriptDataService } from '../../../../core/data/processes/script-data.service';
import { getProcessDetailRoute } from '../../../../process-page/process-page-routing.paths';
import { Process } from '../../../../process-page/processes/process.model';
import { Script } from '../../../../process-page/scripts/script.model';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject$,
} from '../../../../shared/remote-data.utils';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
import { FiltersComponent } from '../../filters-section/filters-section.component';
import { OptionVO } from '../option-vo.model';
import { QueryPredicate } from '../query-predicate.model';
import { FilteredItemsExportCsvComponent } from './filtered-items-export-csv.component';
describe('FilteredItemsExportCsvComponent', () => {
let component: FilteredItemsExportCsvComponent;
let fixture: ComponentFixture<FilteredItemsExportCsvComponent>;
let scriptDataService: ScriptDataService;
let authorizationDataService: AuthorizationDataService;
let notificationsService;
let router;
const script = Object.assign(new Script(), { id: 'metadata-export-filtered-items-report', name: 'metadata-export-filtered-items-report' });
const process = Object.assign(new Process(), { processId: 5, scriptName: 'metadata-export-filtered-items-report' });
const params = new FormGroup({
collections: new FormControl([OptionVO.collection('1', 'coll1')]),
queryPredicates: new FormControl([QueryPredicate.of('name', 'equals', 'coll1')]),
filters: new FormControl([FiltersComponent.getFilter('is_item')]),
});
const emptyParams = new FormGroup({
collections: new FormControl([]),
queryPredicates: new FormControl([]),
filters: new FormControl([]),
});
function initBeforeEachAsync() {
scriptDataService = jasmine.createSpyObj('scriptDataService', {
findById: createSuccessfulRemoteDataObject$(script),
invoke: createSuccessfulRemoteDataObject$(process),
});
authorizationDataService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true),
});
notificationsService = new NotificationsServiceStub();
router = jasmine.createSpyObj('authorizationService', ['navigateByUrl']);
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NgbModule, FilteredItemsExportCsvComponent],
providers: [
{ provide: ScriptDataService, useValue: scriptDataService },
{ provide: AuthorizationDataService, useValue: authorizationDataService },
{ provide: NotificationsService, useValue: notificationsService },
{ provide: Router, useValue: router },
],
}).compileComponents();
}
function initBeforeEach() {
fixture = TestBed.createComponent(FilteredItemsExportCsvComponent);
component = fixture.componentInstance;
component.reportParams = params;
fixture.detectChanges();
}
describe('init', () => {
describe('comp', () => {
beforeEach(waitForAsync(() => {
initBeforeEachAsync();
}));
beforeEach(() => {
initBeforeEach();
});
it('should init the comp', () => {
expect(component).toBeTruthy();
});
});
describe('when the user is an admin and the metadata-export-filtered-items-report script is present ', () => {
beforeEach(waitForAsync(() => {
initBeforeEachAsync();
}));
beforeEach(() => {
initBeforeEach();
});
it('should add the button', () => {
const debugElement = fixture.debugElement.query(By.css('button.export-button'));
expect(debugElement).toBeDefined();
});
});
describe('when the user is not an admin', () => {
beforeEach(waitForAsync(() => {
initBeforeEachAsync();
(authorizationDataService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
}));
beforeEach(() => {
initBeforeEach();
});
it('should not add the button', () => {
const debugElement = fixture.debugElement.query(By.css('button.export-button'));
expect(debugElement).toBeNull();
});
});
describe('when the metadata-export-filtered-items-report script is not present', () => {
beforeEach(waitForAsync(() => {
initBeforeEachAsync();
(scriptDataService.findById as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Not found', 404));
}));
beforeEach(() => {
initBeforeEach();
});
it('should should not add the button', () => {
const debugElement = fixture.debugElement.query(By.css('button.export-button'));
expect(debugElement).toBeNull();
});
});
});
describe('export', () => {
beforeEach(waitForAsync(() => {
initBeforeEachAsync();
}));
beforeEach(() => {
initBeforeEach();
});
it('should call the invoke script method with the correct parameters', () => {
// Parameterized export
component.export();
expect(scriptDataService.invoke).toHaveBeenCalledWith('metadata-export-filtered-items-report',
[
{ name: '-c', value: params.value.collections[0].id },
{ name: '-qp', value: QueryPredicate.toString(params.value.queryPredicates[0]) },
{ name: '-f', value: FiltersComponent.toQueryString(params.value.filters) },
], []);
fixture.detectChanges();
// Non-parameterized export
component.reportParams = emptyParams;
fixture.detectChanges();
component.export();
expect(scriptDataService.invoke).toHaveBeenCalledWith('metadata-export-filtered-items-report', [], []);
});
it('should show a success message when the script was invoked successfully and redirect to the corresponding process page', () => {
component.export();
expect(notificationsService.success).toHaveBeenCalled();
expect(router.navigateByUrl).toHaveBeenCalledWith(getProcessDetailRoute(process.processId));
});
it('should show an error message when the script was not invoked successfully and stay on the current page', () => {
(scriptDataService.invoke as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$('Error', 500));
component.export();
expect(notificationsService.error).toHaveBeenCalled();
expect(router.navigateByUrl).not.toHaveBeenCalled();
});
});
describe('clicking the button', () => {
beforeEach(waitForAsync(() => {
initBeforeEachAsync();
}));
beforeEach(() => {
initBeforeEach();
});
it('should trigger the export function', () => {
spyOn(component, 'export');
const debugElement = fixture.debugElement.query(By.css('button.export-button'));
debugElement.triggerEventHandler('click', null);
expect(component.export).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,123 @@
import { AsyncPipe } from '@angular/common';
import {
Component,
Input,
OnInit,
} from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import {
combineLatest as observableCombineLatest,
Observable,
} from 'rxjs';
import { map } from 'rxjs/operators';
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
import { ScriptDataService } from '../../../../core/data/processes/script-data.service';
import { RemoteData } from '../../../../core/data/remote-data';
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
import { getProcessDetailRoute } from '../../../../process-page/process-page-routing.paths';
import { Process } from '../../../../process-page/processes/process.model';
import { hasValue } from '../../../../shared/empty.util';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { FiltersComponent } from '../../filters-section/filters-section.component';
import { OptionVO } from '../option-vo.model';
import { QueryPredicate } from '../query-predicate.model';
@Component({
selector: 'ds-filtered-items-export-csv',
styleUrls: ['./filtered-items-export-csv.component.scss'],
templateUrl: './filtered-items-export-csv.component.html',
standalone: true,
imports: [NgbTooltipModule, AsyncPipe, TranslateModule],
})
/**
* Display a button to export the MetadataQuery (aka Filtered Items) Report results as csv
*/
export class FilteredItemsExportCsvComponent implements OnInit {
/**
* The current configuration of the search
*/
@Input() reportParams: FormGroup;
/**
* Observable used to determine whether the button should be shown
*/
shouldShowButton$: Observable<boolean>;
/**
* The message key used for the tooltip of the button
*/
tooltipMsg = 'metadata-export-filtered-items.tooltip';
constructor(private scriptDataService: ScriptDataService,
private authorizationDataService: AuthorizationDataService,
private notificationsService: NotificationsService,
private translateService: TranslateService,
private router: Router,
) {
}
static csvExportEnabled(scriptDataService: ScriptDataService, authorizationDataService: AuthorizationDataService): Observable<boolean> {
const scriptExists$ = scriptDataService.findById('metadata-export-filtered-items-report').pipe(
getFirstCompletedRemoteData(),
map((rd) => rd.isSuccess && hasValue(rd.payload)),
);
const isAuthorized$ = authorizationDataService.isAuthorized(FeatureID.AdministratorOf);
return observableCombineLatest([scriptExists$, isAuthorized$]).pipe(
map(([scriptExists, isAuthorized]: [boolean, boolean]) => scriptExists && isAuthorized),
);
}
ngOnInit(): void {
this.shouldShowButton$ = FilteredItemsExportCsvComponent.csvExportEnabled(this.scriptDataService, this.authorizationDataService);
}
/**
* Start the export of the items based on the selected parameters
*/
export() {
const parameters = [];
const colls = this.reportParams.value.collections || [];
for (let i = 0; i < colls.length; i++) {
if (colls[i]) {
parameters.push({ name: '-c', value: OptionVO.toString(colls[i]) });
}
}
const preds = this.reportParams.value.queryPredicates || [];
for (let i = 0; i < preds.length; i++) {
const field = preds[i].field;
const op = preds[i].operator;
if (field && op) {
parameters.push({ name: '-qp', value: QueryPredicate.toString(preds[i]) });
}
}
const filters = FiltersComponent.toQueryString(this.reportParams.value.filters) || [];
if (filters.length > 0) {
parameters.push({ name: '-f', value: filters });
}
this.scriptDataService.invoke('metadata-export-filtered-items-report', parameters, []).pipe(
getFirstCompletedRemoteData(),
).subscribe((rd: RemoteData<Process>) => {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get('metadata-export-filtered-items.submit.success'));
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
} else {
this.notificationsService.error(this.translateService.get('metadata-export-filtered-items.submit.error'));
}
});
}
}

View File

@@ -1,8 +1,10 @@
import { Item } from 'src/app/core/shared/item.model';
import { Collection } from '../../../core/shared/collection.model';
export class FilteredItems {
public items: Item[] = [];
public items: FilteredItem[] = [];
public itemCount: number;
public clear() {
@@ -21,3 +23,8 @@ export class FilteredItems {
}
}
export interface FilteredItem extends Omit<Item, 'owningCollection'> {
index: number;
owningCollection?: Collection;
}

View File

@@ -1,175 +1,210 @@
<div class="container">
<div class="metadata-registry row">
<div class="col-12">
<div class="metadata-registry row">
<div class="col-12">
<h1 id="header" class="border-bottom pb-2">{{'admin.reports.items.head' | translate}}</h1>
<h1 id="header" class="border-bottom pb-2">{{'admin.reports.items.head' | translate}}</h1>
<div id="querydiv" [formGroup]="queryForm">
<ngb-accordion [closeOthers]="true" activeIds="collectionSelector" #acc="ngbAccordion">
<ngb-panel id="collectionSelector">
<ng-template ngbPanelTitle>
{{'admin.reports.items.section.collectionSelector' | translate}}
</ng-template>
<ng-template ngbPanelContent>
<select id="collSel" name="collSel" class="form-control" multiple="multiple" size="10" formControlName="collections">
<option *ngFor="let item of collections" [value]="item.id" [disabled]="item.disabled">{{item.name$ | async}}</option>
</select>
<div class="row">
<span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{'admin.reports.items.run' | translate}}</button>
</div>
</ng-template>
</ngb-panel>
<ngb-panel id="metadataFieldQueries">
<ng-template ngbPanelTitle>
{{'admin.reports.items.section.metadataFieldQueries' | translate}}
</ng-template>
<ng-template ngbPanelContent>
<fieldset id="predefqueries" class="form-group">
<label>
{{'admin.reports.items.predefinedQueries' | translate}}
</label>
<select id="predefselect" formControlName="presetQuery" class="form-control" (change)="setPresetQuery()">
<option *ngFor="let item of presetQueries" [value]="item.id" [selected]="item.isDefault">{{item.label | translate}}</option>
</select>
</fieldset>
<div class="row">&nbsp;</div>
<div id="queries">
<div class="metadata" *ngFor="let pred of queryPredicatesArray().controls; let i = index">
<div [formGroup]="pred" class="form-group">
<div class="form-row">
<div class="col-4">
<select class="query-tool" formControlName="field" class="form-control">
<option *ngFor="let item of metadataFieldsWithAny" [value]="item.id">{{item.name$ | async}}</option>
</select>
</div>
<div class="col-auto">
<select class="query-tool" formControlName="operator" class="form-control">
<option *ngFor="let item of predicates" [value]="item.id">{{item.name$ | async | translate}}</option>
</select>
</div>
<div class="col">
<input class="form-control" formControlName="value"/>
</div>
<div class="col-auto">
<button class="btn btn-light" (click)="addQueryPredicate()">+</button>
&nbsp;
<button class="btn btn-light" [dsBtnDisabled]="deleteQueryPredicateDisabled()" (click)="deleteQueryPredicate(i)"></button>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{'admin.reports.items.run' | translate}}</button>
</div>
</ng-template>
</ngb-panel>
<ngb-panel id="limitPaginateQueries">
<ng-template ngbPanelTitle>
{{'admin.reports.items.section.limitPaginateQueries' | translate}}
</ng-template>
<ng-template ngbPanelContent>
<div class="row align-items-center">
<label for="limit" class="col-sm-2 col-form-label">{{'admin.reports.items.limit' | translate}}:</label>
<div class="col-6">
<select id="limit" name="limit" formControlName="pageLimit" class="form-control col-6">
<option *ngFor="let item of pageLimits" value="{{item.id}}" [selected]="item.isDefault">{{item.name$ | async}}</option>
</select>
</div>
</div>
<div class="row align-items-center">
<label for="offset" class="col-sm-2 col-form-label">{{'admin.reports.items.offset' | translate}}:</label>
<div class="col-6">
<input id="offset" name="offset" value="0" class="form-control col-6">
</div>
</div>
<div class="row">
<span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{'admin.reports.items.run' | translate}}</button>
</div>
</ng-template>
</ngb-panel>
<ngb-panel id="filters">
<ng-template ngbPanelTitle>
{{'admin.reports.commons.filters' | translate}}
</ng-template>
<ng-template ngbPanelContent>
<div class="row">
<span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{'admin.reports.items.run' | translate}}</button>
</div>
<ds-filters [filtersForm]="filtersFormGroup()"></ds-filters>
<div class="row">
<span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{'admin.reports.items.run' | translate}}</button>
</div>
</ng-template>
</ngb-panel>
<ngb-panel id="additionalData">
<ng-template ngbPanelTitle>
{{'admin.reports.commons.additional-data' | translate}}
</ng-template>
<ng-template ngbPanelContent>
<div id="show-fields">
<select class="query-tool" name="show_fields" multiple="multiple" size="8" class="form-control" formControlName="additionalFields">
<option *ngFor="let item of metadataFields" [value]="item.id">{{item.name$ | async}}</option>
</select>
</div>
<div class="row">
<span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{'admin.reports.items.run' | translate}}</button>
</div>
</ng-template>
</ngb-panel>
<ngb-panel id="itemResults">
<ng-template ngbPanelTitle>
{{'admin.reports.collections.item-results' | translate}}
</ng-template>
<ng-template ngbPanelContent>
<table id="table" class="table table-striped">
<thead>
<tr class="header">
<th>{{ "admin.reports.items.number" | translate }}</th>
<th>{{ "admin.reports.items.id" | translate }}</th>
<th>{{ "admin.reports.items.collection" | translate }}</th>
<th>{{ "admin.reports.items.handle" | translate }}</th>
<th>{{ "admin.reports.items.title" | translate }}</th>
<th *ngFor="let field of queryForm.value['additionalFields']">{{ field }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of results$ | async">
<td class="num">{{ item.index }}</td>
<td>{{ item.uuid }}</td>
<td><a *ngIf="item.owningCollection" href="/handle/{{ item.owningCollection.handle }}" rel="noopener noreferrer" target="_blank">{{ item.owningCollection.name }}</a></td>
<td><a *ngIf="item.handle" href="/handle/{{ item.handle }}" rel="noopener noreferrer" target="_blank">{{ item.handle }}</a></td>
<td>{{ item.name }}</td>
<td class="num" *ngFor="let field of queryForm.value['additionalFields']">
<span *ngFor="let mdvalue of item.metadata[field]">
{{ mdvalue.value || "" }}
</span>
</td>
</tr>
</tbody>
</table>
<div>
{{'admin.reports.commons.page' | translate}} {{ currentPage + 1 }} {{'admin.reports.commons.of' | translate}} {{ pageCount() }}
</div>
<div>
<button id="prev" class="btn btn-light" (click)="prevPage()" [dsBtnDisabled]="!canNavigatePrevious()">{{'admin.reports.commons.previous-page' | translate}}</button>
<button id="next" class="btn btn-light" (click)="nextPage()" [dsBtnDisabled]="!canNavigateNext()">{{'admin.reports.commons.next-page' | translate}}</button>
<!--
<button id="export">{{'admin.reports.commons.export' | translate}}</button>
-->
</div>
<table id="itemtable" class="sortable"></table>
</ng-template>
</ngb-panel>
</ngb-accordion>
</div>
<div id="querydiv" [formGroup]="queryForm">
<ngb-accordion [closeOthers]="true" activeIds="collectionSelector" #acc="ngbAccordion">
<ngb-panel id="collectionSelector">
<ng-template ngbPanelTitle>
{{'admin.reports.items.section.collectionSelector' | translate}}
</ng-template>
<ng-template ngbPanelContent>
@if (loadingCollections$ | async) {
<ds-loading></ds-loading>
}
@if ((loadingCollections$ | async) !== true) {
<select id="collSel" name="collSel" class="form-control" multiple="multiple" size="10" formControlName="collections">
@for (item of collections; track item) {
<option [value]="item.id" [disabled]="item.disabled">{{item.name$ | async}}</option>
}
</select>
}
<div class="row">
<span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{'admin.reports.items.run' | translate}}</button>
</div>
</ng-template>
</ngb-panel>
<ngb-panel id="metadataFieldQueries">
<ng-template ngbPanelTitle>
{{'admin.reports.items.section.metadataFieldQueries' | translate}}
</ng-template>
<ng-template ngbPanelContent>
<fieldset id="predefqueries" class="form-group">
<label>
{{'admin.reports.items.predefinedQueries' | translate}}
</label>
<select id="predefselect" formControlName="presetQuery" class="form-control" (change)="setPresetQuery()">
@for (item of presetQueries; track item.id) {
<option [value]="item.id" [selected]="item.isDefault">{{item.label | translate}}</option>
}
</select>
</fieldset>
<div class="row">&nbsp;</div>
<div id="queries">
@for (pred of queryPredicatesArray().controls; track pred; let i = $index) {
<div class="metadata">
<div [formGroup]="pred" class="form-group">
<div class="form-row">
<div class="col-4">
<select class="query-tool" formControlName="field" class="form-control">
@for (item of metadataFieldsWithAny; track item) {
<option [value]="item.id">{{item.name$ | async}}</option>
}
</select>
</div>
<div class="col-auto">
<select class="query-tool" formControlName="operator" class="form-control">
@for (item of predicates; track item) {
<option [value]="item.id">{{item.name$ | async | translate}}</option>
}
</select>
</div>
<div class="col">
<input class="form-control" formControlName="value"/>
</div>
<div class="col-auto">
<button class="btn btn-light" (click)="addQueryPredicate()">+</button>
&nbsp;
<button class="btn btn-light" [dsBtnDisabled]="deleteQueryPredicateDisabled()" (click)="deleteQueryPredicate(i)"></button>
</div>
</div>
</div>
</div>
}
</div>
<div class="row">
<span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{'admin.reports.items.run' | translate}}</button>
</div>
</ng-template>
</ngb-panel>
<ngb-panel id="limitPaginateQueries">
<ng-template ngbPanelTitle>
{{'admin.reports.items.section.limitPaginateQueries' | translate}}
</ng-template>
<ng-template ngbPanelContent>
<div class="row align-items-center">
<label for="limit" class="col-sm-2 col-form-label">{{'admin.reports.items.limit' | translate}}:</label>
<div class="col-6">
<select id="limit" name="limit" formControlName="pageLimit" class="form-control col-6">
@for (item of pageLimits; track item) {
<option value="{{item.id}}" [selected]="item.isDefault">{{item.name$ | async}}</option>
}
</select>
</div>
</div>
<div class="row align-items-center">
<label for="offset" class="col-sm-2 col-form-label">{{'admin.reports.items.offset' | translate}}:</label>
<div class="col-6">
<input id="offset" name="offset" value="0" class="form-control col-6">
</div>
</div>
<div class="row">
<span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{'admin.reports.items.run' | translate}}</button>
</div>
</ng-template>
</ngb-panel>
<ngb-panel id="filters">
<ng-template ngbPanelTitle>
{{'admin.reports.commons.filters' | translate}}
</ng-template>
<ng-template ngbPanelContent>
<div class="row">
<span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{'admin.reports.items.run' | translate}}</button>
</div>
<ds-filters [filtersForm]="filtersFormGroup()"></ds-filters>
<div class="row">
<span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{'admin.reports.items.run' | translate}}</button>
</div>
</ng-template>
</ngb-panel>
<ngb-panel id="additionalData">
<ng-template ngbPanelTitle>
{{'admin.reports.commons.additional-data' | translate}}
</ng-template>
<ng-template ngbPanelContent>
<div id="show-fields">
<select class="query-tool" name="show_fields" multiple="multiple" size="8" class="form-control" formControlName="additionalFields">
@for (item of metadataFields; track item) {
<option [value]="item.id">{{item.name$ | async}}</option>
}
</select>
</div>
<div class="row">
@if (csvExportEnabled$ | async) {
<span class="col-3"></span>
<div class="warning">{{ 'metadata-export-filtered-items.columns.warning' | translate }}</div>
}
<span class="col-3"></span>
<button class="btn btn-primary mt-1 col-6" (click)="submit()">{{'admin.reports.items.run' | translate}}</button>
</div>
</ng-template>
</ngb-panel>
<ngb-panel id="itemResults">
<ng-template ngbPanelTitle>
{{'admin.reports.collections.item-results' | translate}}
</ng-template>
<ng-template ngbPanelContent>
<table id="table" class="table table-striped">
<thead>
<tr class="header">
<th>{{ "admin.reports.items.number" | translate }}</th>
<th>{{ "admin.reports.items.id" | translate }}</th>
<th>{{ "admin.reports.items.collection" | translate }}</th>
<th>{{ "admin.reports.items.handle" | translate }}</th>
<th>{{ "admin.reports.items.title" | translate }}</th>
@for (field of queryForm.value['additionalFields']; track field) {
<th>{{ field }}</th>
}
</tr>
</thead>
<tbody>
@for (item of results$ | async; track item) {
<tr>
<td class="num">{{ item.index }}</td>
<td>{{ item.uuid }}</td>
<td>@if (item.owningCollection) {
<a href="/handle/{{ item.owningCollection.handle }}" rel="noopener noreferrer" target="_blank">{{ item.owningCollection.name }}</a>
}</td>
<td>@if (item.handle) {
<a href="/handle/{{ item.handle }}" rel="noopener noreferrer" target="_blank">{{ item.handle }}</a>
}</td>
<td>{{ item.name }}</td>
@for (field of queryForm.value['additionalFields']; track field) {
<td class="num">
@for (mdvalue of item.metadata[field]; track mdvalue) {
<span>
{{ mdvalue.value || "" }}
</span>
}
</td>
}
</tr>
}
</tbody>
</table>
<div>
{{'admin.reports.commons.page' | translate}} {{ currentPage + 1 }} {{'admin.reports.commons.of' | translate}} {{ pageCount() }}
</div>
<div>
<button id="prev" class="btn btn-light" (click)="prevPage()" [dsBtnDisabled]="!canNavigatePrevious()">{{'admin.reports.commons.previous-page' | translate}}</button>
<button id="next" class="btn btn-light" (click)="nextPage()" [dsBtnDisabled]="!canNavigateNext()">{{'admin.reports.commons.next-page' | translate}}</button>
<div style="float: right; margin-right: 60px;">
<ds-filtered-items-export-csv [reportParams]="queryForm"></ds-filtered-items-export-csv>
</div>
</div>
<table id="itemtable" class="sortable"></table>
</ng-template>
</ngb-panel>
</ngb-accordion>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,3 +1,10 @@
.num {
text-align: center;
}
.warning {
color: red;
font-style: italic;
text-align: center;
width: 100%;
}

View File

@@ -1,8 +1,4 @@
import {
AsyncPipe,
NgForOf,
NgIf,
} from '@angular/common';
import { AsyncPipe } from '@angular/common';
import {
Component,
OnInit,
@@ -24,13 +20,16 @@ import {
TranslateService,
} from '@ngx-translate/core';
import {
BehaviorSubject,
map,
Observable,
} from 'rxjs';
import { CollectionDataService } from 'src/app/core/data/collection-data.service';
import { CommunityDataService } from 'src/app/core/data/community-data.service';
import { AuthorizationDataService } from 'src/app/core/data/feature-authorization/authorization-data.service';
import { MetadataFieldDataService } from 'src/app/core/data/metadata-field-data.service';
import { MetadataSchemaDataService } from 'src/app/core/data/metadata-schema-data.service';
import { ScriptDataService } from 'src/app/core/data/processes/script-data.service';
import { RestRequestMethod } from 'src/app/core/data/rest-request-method';
import { DspaceRestService } from 'src/app/core/dspace-rest/dspace-rest.service';
import { RawRestResponse } from 'src/app/core/dspace-rest/raw-rest-response.model';
@@ -38,14 +37,18 @@ import { MetadataField } from 'src/app/core/metadata/metadata-field.model';
import { MetadataSchema } from 'src/app/core/metadata/metadata-schema.model';
import { Collection } from 'src/app/core/shared/collection.model';
import { Community } from 'src/app/core/shared/community.model';
import { Item } from 'src/app/core/shared/item.model';
import { getFirstSucceededRemoteListPayload } from 'src/app/core/shared/operators';
import { isEmpty } from 'src/app/shared/empty.util';
import { ThemedLoadingComponent } from 'src/app/shared/loading/themed-loading.component';
import { environment } from 'src/environments/environment';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { FiltersComponent } from '../filters-section/filters-section.component';
import { FilteredItems } from './filtered-items-model';
import { FilteredItemsExportCsvComponent } from './filtered-items-export-csv/filtered-items-export-csv.component';
import {
FilteredItem,
FilteredItems,
} from './filtered-items-model';
import { OptionVO } from './option-vo.model';
import { PresetQuery } from './preset-query.model';
import { QueryPredicate } from './query-predicate.model';
@@ -62,16 +65,21 @@ import { QueryPredicate } from './query-predicate.model';
NgbAccordionModule,
TranslateModule,
AsyncPipe,
NgIf,
NgForOf,
FiltersComponent,
BtnDisabledDirective,
FilteredItemsExportCsvComponent,
ThemedLoadingComponent,
],
standalone: true,
})
export class FilteredItemsComponent implements OnInit {
collections: OptionVO[];
/**
* A Boolean representing if loading the list of collections is pending
*/
loadingCollections$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
presetQueries: PresetQuery[];
metadataFields: OptionVO[];
metadataFieldsWithAny: OptionVO[];
@@ -81,8 +89,12 @@ export class FilteredItemsComponent implements OnInit {
queryForm: FormGroup;
currentPage = 0;
results: FilteredItems = new FilteredItems();
results$: Observable<Item[]>;
results$: Observable<FilteredItem[]>;
@ViewChild('acc') accordionComponent: NgbAccordion;
/**
* Observable used to determine whether CSV export is enabled
*/
csvExportEnabled$: Observable<boolean>;
constructor(
private communityService: CommunityDataService,
@@ -90,6 +102,8 @@ export class FilteredItemsComponent implements OnInit {
private metadataSchemaService: MetadataSchemaDataService,
private metadataFieldService: MetadataFieldDataService,
private translateService: TranslateService,
private scriptDataService: ScriptDataService,
private authorizationDataService: AuthorizationDataService,
private formBuilder: FormBuilder,
private restService: DspaceRestService) {}
@@ -104,6 +118,8 @@ export class FilteredItemsComponent implements OnInit {
new QueryPredicate().toFormGroup(this.formBuilder),
];
this.csvExportEnabled$ = FilteredItemsExportCsvComponent.csvExportEnabled(this.scriptDataService, this.authorizationDataService);
this.queryForm = this.formBuilder.group({
collections: this.formBuilder.control([''], []),
presetQuery: this.formBuilder.control('new', []),
@@ -115,6 +131,7 @@ export class FilteredItemsComponent implements OnInit {
}
loadCollections(): void {
this.loadingCollections$.next(true);
this.collections = [];
const wholeRepo$ = this.translateService.stream('admin.reports.items.wholeRepo');
this.collections.push(OptionVO.collectionLoc('', wholeRepo$));
@@ -136,6 +153,7 @@ export class FilteredItemsComponent implements OnInit {
const collVO = OptionVO.collection(collection.uuid, '' + collection.name);
this.collections.push(collVO);
});
this.loadingCollections$.next(false);
},
);
});
@@ -171,10 +189,10 @@ export class FilteredItemsComponent implements OnInit {
QueryPredicate.of('dc.description.provenance', QueryPredicate.DOES_NOT_MATCH, '^.*No\. of bitstreams(.|\r|\n|\r\n)*\.(PDF|pdf|DOC|doc|PPT|ppt|DOCX|docx|PPTX|pptx).*$'),
]),
PresetQuery.of('q9', 'admin.reports.items.preset.hasEmptyMetadata', [
QueryPredicate.of('*', QueryPredicate.MATCHES, '^\s*$'),
QueryPredicate.of('*', QueryPredicate.MATCHES, '^\\s*$'),
]),
PresetQuery.of('q10', 'admin.reports.items.preset.hasUnbreakingDataInDescription', [
QueryPredicate.of('dc.description.*', QueryPredicate.MATCHES, '^.*[^\s]{50,}.*$'),
QueryPredicate.of('dc.description.*', QueryPredicate.MATCHES, '^.*(\\S){50,}.*$'),
]),
PresetQuery.of('q12', 'admin.reports.items.preset.hasXmlEntityInMetadata', [
QueryPredicate.of('*', QueryPredicate.MATCHES, '^.*&#.*$'),
@@ -348,13 +366,8 @@ export class FilteredItemsComponent implements OnInit {
const preds = this.queryForm.value.queryPredicates;
for (let i = 0; i < preds.length; i++) {
const field = preds[i].field;
const op = preds[i].operator;
const value = preds[i].value;
params += `&queryPredicates=${field}:${op}`;
if (value) {
params += `:${value}`;
}
const pred = encodeURIComponent(QueryPredicate.toString(preds[i]));
params += `&queryPredicates=${pred}`;
}
const filters = FiltersComponent.toQueryString(this.queryForm.value.filters);

View File

@@ -9,6 +9,7 @@ export class OptionVO {
id: string;
name$: Observable<string>;
disabled = false;
isDefault?: boolean;
static collection(id: string, name: string, disabled: boolean = false): OptionVO {
const opt = new OptionVO();
@@ -45,6 +46,16 @@ export class OptionVO {
subscriber.next(value);
subscriber.complete();
});
}
static toString(obj: any): string {
if (obj) {
if (obj instanceof OptionVO && obj.id) {
return obj.id;
}
return obj as string;
}
return '';
}
}

View File

@@ -5,6 +5,7 @@ export class PresetQuery {
id: string;
label: string;
predicates: QueryPredicate[];
isDefault?: boolean;
static of(id: string, label: string, predicates: QueryPredicate[]) {
const query = new PresetQuery();

View File

@@ -29,6 +29,13 @@ export class QueryPredicate {
return pred;
}
static toString(pred: QueryPredicate): string {
if (pred.value) {
return `${pred.field}:${pred.operator}:${pred.value}`;
}
return `${pred.field}:${pred.operator}`;
}
toFormGroup(formBuilder: FormBuilder): FormGroup {
return formBuilder.group({
field: new FormControl(this.field),

View File

@@ -1,19 +1,23 @@
<fieldset class="row row-cols-2">
<ng-container>
<span class="col-12">&nbsp;</span>
<span class="col-1">&nbsp;</span>
<button id="btnSelectAllFilters" class="btn btn-light mt-1 col-4" (click)="selectAll()">{{"admin.reports.commons.filters.select_all" | translate}}</button>
<span class="col-2">&nbsp;</span>
<button id="btnDeselectAllFilters" class="btn btn-light mt-1 col-4" (click)="deselectAll()">{{"admin.reports.commons.filters.deselect_all" | translate}}</button>
<span class="col-1">&nbsp;</span>
<span class="col-12">&nbsp;</span>
</ng-container>
<ng-container>
<span class="col-12">&nbsp;</span>
<span class="col-1">&nbsp;</span>
<button id="btnSelectAllFilters" class="btn btn-light mt-1 col-4" (click)="selectAll()">{{"admin.reports.commons.filters.select_all" | translate}}</button>
<span class="col-2">&nbsp;</span>
<button id="btnDeselectAllFilters" class="btn btn-light mt-1 col-4" (click)="deselectAll()">{{"admin.reports.commons.filters.deselect_all" | translate}}</button>
<span class="col-1">&nbsp;</span>
<span class="col-12">&nbsp;</span>
</ng-container>
</fieldset>
<fieldset class="row row-cols-2" *ngFor="let group of allFilters()">
@for (group of allFilters(); track group) {
<fieldset class="row row-cols-2">
<legend>{{group.key | translate}}</legend>
<ng-container [formGroup]="filtersForm">
<div *ngFor="let filter of group.filters" class="col-6">
<input type="checkbox" id="flt-{{filter.id}}" value="{{filter.id}}" class="form-check-input col-1 align-baseline" formControlName="{{filter.id}}"><label for="flt-{{filter.id}}" class="col-11 align-middle" title="{{filter.tooltipKey | translate}}">{{filter.key | translate}}</label>
@for (filter of group.filters; track filter) {
<div class="col-6">
<input type="checkbox" id="flt-{{filter.id}}" value="{{filter.id}}" class="form-check-input col-1" formControlName="{{filter.id}}"><label for="flt-{{filter.id}}" class="col-11 align-middle" title="{{filter.tooltipKey | translate}}">{{filter.key | translate}}</label>
</div>
}
</ng-container>
</fieldset>
</fieldset>
}

View File

@@ -0,0 +1,8 @@
.col-6 > label.col-11.align-middle {
padding-left: 15px;
padding-right: 15px;
}
fieldset.row-cols-2 > legend {
float: none !important;
}

View File

@@ -1,4 +1,4 @@
import { NgForOf } from '@angular/common';
import {
Component,
Input,
@@ -23,7 +23,6 @@ import { FilterGroup } from './filter-group.model';
templateUrl: './filters-section.component.html',
styleUrls: ['./filters-section.component.scss'],
imports: [
NgForOf,
ReactiveFormsModule,
TranslateModule,
],

View File

@@ -10,7 +10,6 @@ import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { AuthService } from '../../../../../core/auth/auth.service';
import { AccessStatusDataService } from '../../../../../core/data/access-status-data.service';
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
import { RemoteData } from '../../../../../core/data/remote-data';
@@ -22,7 +21,6 @@ import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service';
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
import { AccessStatusObject } from '../../../../../shared/object-collection/shared/badges/access-status-badge/access-status.model';
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub';
@@ -44,12 +42,6 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
},
};
const mockAccessStatusDataService = {
findAccessStatusFor(item: Item): Observable<RemoteData<AccessStatusObject>> {
return createSuccessfulRemoteDataObject$(new AccessStatusObject());
},
};
const mockThemeService = getMockThemeService();
function init() {
@@ -74,7 +66,6 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
{ provide: TruncatableService, useValue: mockTruncatableService },
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
{ provide: ThemeService, useValue: mockThemeService },
{ provide: AccessStatusDataService, useValue: mockAccessStatusDataService },
{ provide: AuthService, useClass: AuthServiceStub },
{ provide: FileService, useClass: FileServiceStub },
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },

View File

@@ -1,30 +1,52 @@
<div class="space-children-mr my-1">
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary move-link" [routerLink]="[getMoveRoute()]" [title]="'admin.search.item.move' | translate">
<i class="fa fa-arrow-circle-right"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span>
</a>
<i class="fa fa-arrow-circle-right"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span>
}
</a>
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isDiscoverable" class="btn btn-secondary private-link" [routerLink]="[getPrivateRoute()]" [title]="'admin.search.item.make-private' | translate">
<i class="fa fa-eye-slash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-private" | translate}}</span>
</a>
@if (item && item.isDiscoverable) {
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary private-link" [routerLink]="[getPrivateRoute()]" [title]="'admin.search.item.make-private' | translate">
<i class="fa fa-eye-slash"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.search.item.make-private" | translate}}</span>
}
</a>
}
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isDiscoverable" class="btn btn-secondary public-link" [routerLink]="[getPublicRoute()]" [title]="'admin.search.item.make-public' | translate">
<i class="fa fa-eye"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-public" | translate}}</span>
</a>
@if (item && !item.isDiscoverable) {
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary public-link" [routerLink]="[getPublicRoute()]" [title]="'admin.search.item.make-public' | translate">
<i class="fa fa-eye"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.search.item.make-public" | translate}}</span>
}
</a>
}
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary edit-link" [routerLink]="[getEditRoute()]" [title]="'admin.search.item.edit' | translate">
<i class="fa fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span>
</a>
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary edit-link" [routerLink]="[getEditRoute()]" [title]="'admin.search.item.edit' | translate">
<i class="fa fa-edit"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span>
}
</a>
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isWithdrawn" class="btn btn-warning t withdraw-link" [routerLink]="[getWithdrawRoute()]" [title]="'admin.search.item.withdraw' | translate">
<i class="fa fa-ban"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span>
</a>
@if (item && !item.isWithdrawn) {
<a [ngClass]="{'btn-sm': small}" class="btn btn-warning t withdraw-link" [routerLink]="[getWithdrawRoute()]" [title]="'admin.search.item.withdraw' | translate">
<i class="fa fa-ban"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span>
}
</a>
}
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isWithdrawn" class="btn btn-warning reinstate-link" [routerLink]="[getReinstateRoute()]" [title]="'admin.search.item.reinstate' | translate">
<i class="fa fa-undo"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.reinstate" | translate}}</span>
</a>
@if (item && item.isWithdrawn) {
<a [ngClass]="{'btn-sm': small}" class="btn btn-warning reinstate-link" [routerLink]="[getReinstateRoute()]" [title]="'admin.search.item.reinstate' | translate">
<i class="fa fa-undo"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.search.item.reinstate" | translate}}</span>
}
</a>
}
<a [ngClass]="{'btn-sm': small}" class="btn btn-danger delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.search.item.delete' | translate">
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.delete" | translate}}</span>
</a>
<a [ngClass]="{'btn-sm': small}" class="btn btn-danger delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.search.item.delete' | translate">
<i class="fa fa-trash"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.search.item.delete" | translate}}</span>
}
</a>
</div>

View File

@@ -1,7 +1,4 @@
import {
NgClass,
NgIf,
} from '@angular/common';
import { NgClass } from '@angular/common';
import {
Component,
Input,
@@ -26,7 +23,7 @@ import { getItemEditRoute } from '../../../item-page/item-page-routing-paths';
styleUrls: ['./item-admin-search-result-actions.component.scss'],
templateUrl: './item-admin-search-result-actions.component.html',
standalone: true,
imports: [NgClass, RouterLink, NgIf, TranslateModule],
imports: [NgClass, RouterLink, TranslateModule],
})
/**
* The component for displaying the actions for a list element for an item search result on the admin search page

View File

@@ -3,7 +3,7 @@
[ngClass]="{ disabled: isDisabled }"
role="menuitem"
[attr.aria-disabled]="isDisabled"
[attr.aria-labelledby]="adminMenuSectionTitleId(section.id)"
[attr.aria-labelledby]="adminMenuSectionTitleAccessibilityHandle(section)"
[routerLink]="itemModel.link"
(keyup.space)="navigate($event)"
(keyup.enter)="navigate($event)"
@@ -14,7 +14,7 @@
</div>
<div class="sidebar-collapsible-element-outer-wrapper">
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item">
<span [id]="adminMenuSectionTitleId(section.id)" [attr.data-test]="adminMenuSectionTitleId(section.id) | dsBrowserOnly">
<span [id]="adminMenuSectionTitleAccessibilityHandle(section)" [attr.data-test]="adminMenuSectionTitleAccessibilityHandle(section) | dsBrowserOnly">
{{itemModel.text | translate}}
</span>
</div>

View File

@@ -16,7 +16,7 @@ import { MenuService } from '../../../shared/menu/menu.service';
import { MenuID } from '../../../shared/menu/menu-id.model';
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
import { MenuSection } from '../../../shared/menu/menu-section.model';
import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-section.component';
import { AbstractMenuSectionComponent } from '../../../shared/menu/menu-section/abstract-menu-section.component';
import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
/**
@@ -30,7 +30,7 @@ import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
imports: [NgClass, RouterLink, TranslateModule, BrowserOnlyPipe],
})
export class AdminSidebarSectionComponent extends MenuSectionComponent implements OnInit {
export class AdminSidebarSectionComponent extends AbstractMenuSectionComponent implements OnInit {
/**
* This section resides in the Admin Sidebar
@@ -44,16 +44,17 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
isDisabled: boolean;
constructor(
@Inject('sectionDataProvider') menuSection: MenuSection,
@Inject('sectionDataProvider') protected section: MenuSection,
protected menuService: MenuService,
protected injector: Injector,
protected router: Router,
) {
super(menuSection, menuService, injector);
this.itemModel = menuSection.model as LinkMenuItemModel;
super(menuService, injector);
this.itemModel = section.model as LinkMenuItemModel;
}
ngOnInit(): void {
// todo: should support all menu entries?
this.isDisabled = this.itemModel?.disabled || isEmpty(this.itemModel?.link);
super.ngOnInit();
}
@@ -65,11 +66,13 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
}
}
adminMenuSectionId(sectionId: string) {
return `admin-menu-section-${sectionId}`;
adminMenuSectionId(section: MenuSection) {
const accessibilityHandle = section.accessibilityHandle ?? section.id;
return `admin-menu-section-${accessibilityHandle}`;
}
adminMenuSectionTitleId(sectionId: string) {
return `admin-menu-section-${sectionId}-title`;
adminMenuSectionTitleAccessibilityHandle(section: MenuSection) {
const accessibilityHandle = section.accessibilityHandle ?? section.id;
return `admin-menu-section-${accessibilityHandle}-title`;
}
}

View File

@@ -1,64 +1,62 @@
<nav class="navbar navbar-dark p-0 vh-100"
id="admin-sidebar"
[attr.aria-label]="'menu.header.nav.description' | translate"
[ngClass]="{'expanded': sidebarOpen, 'collapsed': sidebarClosed, 'transitioning': sidebarTransitioning}"
@if (menuVisible | async) {
<nav class="navbar navbar-dark p-0 vh-100"
id="admin-sidebar"
[attr.aria-label]="'menu.header.nav.description' | translate"
[ngClass]="{'expanded': sidebarOpen, 'collapsed': sidebarClosed, 'transitioning': sidebarTransitioning}"
[@slideSidebar]="{
value: ((sidebarExpanded | async) !== true ? 'collapsed' : 'expanded'),
params: { collapsedWidth: (collapsedSidebarWidth$ | async), expandedWidth: (expandedSidebarWidth$ | async) }
}" (@slideSidebar.done)="finishSlide($event)" (@slideSidebar.start)="startSlide($event)"
*ngIf="menuVisible | async"
(mouseenter)="handleMouseEnter($event)"
(mouseleave)="handleMouseLeave($event)">
<!-- HEADER -->
<div class="sidebar-full-width-container" id="sidebar-header-container" aria-hidden="true">
<div class="sidebar-section-wrapper">
<div class="sidebar-fixed-element-wrapper">
<img id="admin-sidebar-logo" src="assets/images/dspace-logo-mini.svg" [alt]="('menu.header.image.logo') | translate" aria-hidden="true">
</div>
<div class="sidebar-collapsible-element-outer-wrapper">
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item">
<h4 class="my-1">{{ 'menu.header.admin' | translate }}</h4>
(mouseenter)="handleMouseEnter($event)"
(mouseleave)="handleMouseLeave($event)">
<!-- HEADER -->
<div class="sidebar-full-width-container" id="sidebar-header-container" aria-hidden="true">
<div class="sidebar-section-wrapper">
<div class="sidebar-fixed-element-wrapper">
<img id="admin-sidebar-logo" src="assets/images/dspace-logo-mini.svg" [alt]="('menu.header.image.logo') | translate" aria-hidden="true">
</div>
<div class="sidebar-collapsible-element-outer-wrapper">
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item">
<h4 class="my-1">{{ 'menu.header.admin' | translate }}</h4>
</div>
</div>
</div>
</div>
</div>
<!-- ITEMS -->
<div class="sidebar-full-width-container" id="sidebar-top-level-items-container">
<div class="sidebar-full-width-container" id="sidebar-top-level-items" role="menubar"
<!-- ITEMS -->
<div class="sidebar-full-width-container" id="sidebar-top-level-items-container">
<div class="sidebar-full-width-container" id="sidebar-top-level-items" role="menubar"
[attr.aria-label]="'menu.header.admin.description' |translate">
<ng-container *ngFor="let section of (sections | async)">
<ng-container
@for (section of (sections | async); track section) {
<ng-container
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
</ng-container>
}
</div>
</div>
</div>
<!-- TOGGLER -->
<div class="sidebar-full-width-container" id="sidebar-collapse-toggle-container">
<a class="sidebar-section-wrapper sidebar-full-width-container"
id="sidebar-collapse-toggle"
[attr.data-test]="'sidebar-collapse-toggle' | dsBrowserOnly"
href="javascript:void(0);"
(click)="toggle($event)"
(keyup.space)="toggle($event)"
>
<div class="sidebar-fixed-element-wrapper">
<i *ngIf="(menuCollapsed | async)" class="fas fa-fw fa-angle-double-right"
[title]="'menu.section.icon.pin' | translate"></i>
<i *ngIf="(menuCollapsed | async) !== true" class="fas fa-fw fa-angle-double-left"
[title]="'menu.section.icon.unpin' | translate"></i>
</div>
<div class="sidebar-collapsible-element-outer-wrapper">
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item">
{{ ((menuCollapsed | async) ? 'menu.section.pin' : 'menu.section.unpin' ) | translate }}
<!-- TOGGLER -->
<div class="sidebar-full-width-container" id="sidebar-collapse-toggle-container">
<a class="sidebar-section-wrapper sidebar-full-width-container"
id="sidebar-collapse-toggle"
[attr.data-test]="'sidebar-collapse-toggle' | dsBrowserOnly"
href="javascript:void(0);"
(click)="toggle($event)"
(keyup.space)="toggle($event)"
>
<div class="sidebar-fixed-element-wrapper">
@if ((menuCollapsed | async)) {
<i class="fas fa-fw fa-angle-double-right"
[title]="'menu.section.icon.pin' | translate"></i>
}
@if ((menuCollapsed | async) !== true) {
<i class="fas fa-fw fa-angle-double-left"
[title]="'menu.section.icon.unpin' | translate"></i>
}
</div>
</div>
</a>
</div>
</nav>
<div class="sidebar-collapsible-element-outer-wrapper">
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item">
{{ ((menuCollapsed | async) ? 'menu.section.pin' : 'menu.section.unpin' ) | translate }}
</div>
</div>
</a>
</div>
</nav>
}

View File

@@ -2,8 +2,6 @@ import {
AsyncPipe,
NgClass,
NgComponentOutlet,
NgFor,
NgIf,
} from '@angular/common';
import {
Component,
@@ -47,7 +45,7 @@ import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe';
styleUrls: ['./admin-sidebar.component.scss'],
animations: [slideSidebar],
standalone: true,
imports: [NgIf, NgbDropdownModule, NgClass, NgFor, NgComponentOutlet, AsyncPipe, TranslateModule, BrowserOnlyPipe],
imports: [NgbDropdownModule, NgClass, NgComponentOutlet, AsyncPipe, TranslateModule, BrowserOnlyPipe],
})
export class AdminSidebarComponent extends MenuComponent implements OnInit {
/**

View File

@@ -1,45 +1,52 @@
<div [ngClass]="{'expanded': (isExpanded$ | async)}"
[@bgColor]="{
@if (hasSubSections$ | async) {
<div
[ngClass]="{'expanded': (isExpanded$ | async)}"
[@bgColor]="{
value: ((isExpanded$ | async) ? 'endBackground' : 'startBackground'),
params: {endColor: (sidebarActiveBg$ | async)}
}">
<a class="sidebar-section-wrapper"
role="menuitem" tabindex="0"
aria-haspopup="menu"
[attr.aria-controls]="adminMenuSectionId(section.id)"
[attr.aria-expanded]="isExpanded$ | async"
[attr.aria-label]="('menu.section.toggle.' + section.id) | translate"
[class.disabled]="section.model?.disabled"
(click)="toggleSection($event)"
(keyup.space)="toggleSection($event)"
href="javascript:void(0);"
>
<div class="sidebar-fixed-element-wrapper" data-test="sidebar-section-icon" aria-hidden="true">
<i class="fas fa-{{section.icon}} fa-fw"></i>
</div>
<div class="sidebar-collapsible-element-outer-wrapper">
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item toggler-wrapper">
<span [id]="adminMenuSectionTitleId(section.id)" [attr.data-test]="adminMenuSectionTitleId(section.id) | dsBrowserOnly">
<ng-container
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
</span>
<i class="fas fa-chevron-right fa-xs" aria-hidden="true"
[@rotate]="(isExpanded$ | async) ? 'expanded' : 'collapsed'"
></i>
<a class="sidebar-section-wrapper"
role="menuitem" tabindex="0"
aria-haspopup="menu"
[attr.aria-controls]="adminMenuSectionId(section)"
[attr.aria-expanded]="isExpanded$ | async"
[attr.aria-label]="('menu.section.toggle.' + section.id) | translate"
[class.disabled]="section.model?.disabled"
(click)="toggleSection($event)"
(keyup.space)="toggleSection($event)"
href="javascript:void(0);"
>
<div class="sidebar-fixed-element-wrapper" data-test="sidebar-section-icon" aria-hidden="true">
<i class="fas fa-{{section.icon ?? 'notdef'}} fa-fw"></i>
</div>
</div>
</a>
<div class="sidebar-section-wrapper subsection" @slide *ngIf="(isExpanded$ | async)">
<div class="sidebar-fixed-element-wrapper"></div>
<div class="sidebar-collapsible-element-outer-wrapper">
<div class="sidebar-collapsible-element-inner-wrapper">
<div class="sidebar-sub-level-item-list" role="menu" [id]="adminMenuSectionId(section.id)" [attr.aria-label]="('menu.section.' + section.id) | translate">
<div class="sidebar-item" *ngFor="let subSection of (subSections$ | async)">
<div class="sidebar-collapsible-element-outer-wrapper">
<div class="sidebar-collapsible-element-inner-wrapper sidebar-item toggler-wrapper">
<span [id]="adminMenuSectionTitleAccessibilityHandle(section)" [attr.data-test]="adminMenuSectionTitleAccessibilityHandle(section) | dsBrowserOnly">
<ng-container
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
</span>
<i class="fas fa-chevron-right fa-xs" aria-hidden="true"
[@rotate]="(isExpanded$ | async) ? 'expanded' : 'collapsed'"
></i>
</div>
</div>
</a>
@if ((isExpanded$ | async)) {
<div class="sidebar-section-wrapper subsection" @slide>
<div class="sidebar-fixed-element-wrapper"></div>
<div class="sidebar-collapsible-element-outer-wrapper">
<div class="sidebar-collapsible-element-inner-wrapper">
<div class="sidebar-sub-level-item-list" role="menu" [id]="adminMenuSectionId(section)" [attr.aria-label]="('menu.section.' + section.id) | translate">
@for (subSection of (subSections$ | async); track subSection) {
<div class="sidebar-item">
<ng-container
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
</div>
}
</div>
</div>
</div>
</div>
</div>
}
</div>
</div>
}

View File

@@ -11,6 +11,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs';
import { MenuService } from '../../../shared/menu/menu.service';
import { MenuItemModels } from '../../../shared/menu/menu-section.model';
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
@@ -22,47 +23,89 @@ describe('ExpandableAdminSidebarSectionComponent', () => {
let fixture: ComponentFixture<ExpandableAdminSidebarSectionComponent>;
const menuService = new MenuServiceStub();
const iconString = 'test';
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule, TranslateModule.forRoot(), ExpandableAdminSidebarSectionComponent, TestComponent],
providers: [
{ provide: 'sectionDataProvider', useValue: { icon: iconString, model: {} } },
{ provide: MenuService, useValue: menuService },
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
{ provide: Router, useValue: new RouterStub() },
],
}).compileComponents();
}));
beforeEach(() => {
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
fixture = TestBed.createComponent(ExpandableAdminSidebarSectionComponent);
component = fixture.componentInstance;
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('when there are subsections', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule, TranslateModule.forRoot(), ExpandableAdminSidebarSectionComponent, TestComponent],
providers: [
{ provide: 'sectionDataProvider', useValue: { icon: iconString, model: {} } },
{ provide: MenuService, useValue: menuService },
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
{ provide: Router, useValue: new RouterStub() },
],
}).compileComponents();
}));
it('should set the right icon', () => {
const icon = fixture.debugElement.query(By.css('[data-test="sidebar-section-icon"] > i.fas'));
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
});
describe('when the header text is clicked', () => {
beforeEach(() => {
spyOn(menuService, 'toggleActiveSection');
const sidebarToggler = fixture.debugElement.query(By.css('a.sidebar-section-wrapper'));
sidebarToggler.triggerEventHandler('click', {
preventDefault: () => {/**/
},
});
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([{
id: 'test',
visible: true,
model: {} as MenuItemModels,
}]));
fixture = TestBed.createComponent(ExpandableAdminSidebarSectionComponent);
component = fixture.componentInstance;
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
fixture.detectChanges();
});
it('should call toggleActiveSection on the menuService', () => {
expect(menuService.toggleActiveSection).toHaveBeenCalled();
it('should create', () => {
expect(component).toBeTruthy();
});
it('should set the right icon', () => {
const icon = fixture.debugElement.query(By.css('[data-test="sidebar-section-icon"] > i.fas'));
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
});
describe('when the header text is clicked', () => {
beforeEach(() => {
spyOn(menuService, 'toggleActiveSection');
const sidebarToggler = fixture.debugElement.query(By.css('a.sidebar-section-wrapper'));
sidebarToggler.triggerEventHandler('click', {
preventDefault: () => {/**/
},
});
});
it('should call toggleActiveSection on the menuService', () => {
expect(menuService.toggleActiveSection).toHaveBeenCalled();
});
});
});
describe('when there are no subsections', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [NoopAnimationsModule, TranslateModule.forRoot(), ExpandableAdminSidebarSectionComponent, TestComponent],
providers: [
{ provide: 'sectionDataProvider', useValue: { icon: iconString, model: {} } },
{ provide: MenuService, useValue: menuService },
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
{ provide: Router, useValue: new RouterStub() },
],
}).compileComponents();
}));
beforeEach(() => {
spyOn(menuService, 'getSubSectionsByParentID').and.returnValue(observableOf([]));
fixture = TestBed.createComponent(ExpandableAdminSidebarSectionComponent);
component = fixture.componentInstance;
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should not contain a section', () => {
const icon = fixture.debugElement.query(By.css('.shortcut-icon'));
expect(icon).toBeNull();
const sidebarToggler = fixture.debugElement.query(By.css('.sidebar-section'));
expect(sidebarToggler).toBeNull();
});
});
});

View File

@@ -2,8 +2,6 @@ import {
AsyncPipe,
NgClass,
NgComponentOutlet,
NgFor,
NgIf,
} from '@angular/common';
import {
Component,
@@ -22,8 +20,10 @@ import { map } from 'rxjs/operators';
import { bgColor } from '../../../shared/animations/bgColor';
import { rotate } from '../../../shared/animations/rotate';
import { slide } from '../../../shared/animations/slide';
import { isNotEmpty } from '../../../shared/empty.util';
import { MenuService } from '../../../shared/menu/menu.service';
import { MenuID } from '../../../shared/menu/menu-id.model';
import { MenuSection } from '../../../shared/menu/menu-section.model';
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sidebar-section.component';
@@ -37,7 +37,7 @@ import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sid
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
animations: [rotate, slide, bgColor],
standalone: true,
imports: [NgClass, NgComponentOutlet, NgIf, NgFor, AsyncPipe, TranslateModule, BrowserOnlyPipe],
imports: [NgClass, NgComponentOutlet, AsyncPipe, TranslateModule, BrowserOnlyPipe],
})
export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionComponent implements OnInit {
@@ -67,14 +67,20 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
*/
isExpanded$: Observable<boolean>;
/**
* Emits true when the top section has subsections, else emits false
*/
hasSubSections$: Observable<boolean>;
constructor(
@Inject('sectionDataProvider') menuSection,
@Inject('sectionDataProvider') protected section: MenuSection,
protected menuService: MenuService,
private variableService: CSSVariableService,
protected injector: Injector,
protected router: Router,
) {
super(menuSection, menuService, injector, router);
super(section, menuService, injector, router);
}
/**
@@ -82,6 +88,9 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
*/
ngOnInit(): void {
super.ngOnInit();
this.hasSubSections$ = this.subSections$.pipe(
map((subSections) => isNotEmpty(subSections)),
);
this.sidebarActiveBg$ = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
this.isSidebarCollapsed$ = this.menuService.isMenuCollapsed(this.menuID);
this.isSidebarPreviewCollapsed$ = this.menuService.isMenuPreviewCollapsed(this.menuID);

View File

@@ -1,8 +1,12 @@
<div class="space-children-mr">
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.workflow.item.delete' | translate">
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.workflow.item.delete" | translate}}</span>
</a>
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 send-back-link" [routerLink]="[getSendBackRoute()]" [title]="'admin.workflow.item.send-back' | translate">
<i class="fa fa-hand-point-left"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.workflow.item.send-back" | translate}}</span>
</a>
<i class="fa fa-trash"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.workflow.item.delete" | translate}}</span>
}
</a>
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 send-back-link" [routerLink]="[getSendBackRoute()]" [title]="'admin.workflow.item.send-back' | translate">
<i class="fa fa-hand-point-left"></i>@if (!small) {
<span class="d-none d-sm-inline"> {{"admin.workflow.item.send-back" | translate}}</span>
}
</a>
</div>

View File

@@ -1,7 +1,4 @@
import {
NgClass,
NgIf,
} from '@angular/common';
import { NgClass } from '@angular/common';
import {
Component,
Input,
@@ -20,7 +17,7 @@ import {
styleUrls: ['./workflow-item-admin-workflow-actions.component.scss'],
templateUrl: './workflow-item-admin-workflow-actions.component.html',
standalone: true,
imports: [NgClass, RouterLink, NgIf, TranslateModule],
imports: [NgClass, RouterLink, TranslateModule],
})
/**
* The component for displaying the actions for a list element for a workflow-item on the admin workflow search page

View File

@@ -1,44 +1,47 @@
<div>
<div class="modal-header">{{'supervision-group-selector.header' | translate}}
<button type="button" class="close" (click)="close()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<div class="modal-header">{{'supervision-group-selector.header' | translate}}
<button type="button" class="btn-close" (click)="close()" aria-label="Close">
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="control-group col-sm-12">
<label for="supervisionOrder" class="form-label">{{'supervision-group-selector.select.type-of-order.label' | translate}}</label>
<select name="supervisionOrder" id="supervisionOrder" class="form-select"
[(ngModel)]="selectedOrderType"
attr.aria-label="{{'supervision-group-selector.select.type-of-order.label' | translate}}">
<option value="EDITOR">{{'supervision-group-selector.select.type-of-order.option.editor' | translate}}</option>
<option value="OBSERVER">{{'supervision-group-selector.select.type-of-order.option.observer' | translate}}</option>
</select>
@if (isSubmitted && (!selectedOrderType || selectedOrderType === '')) {
<ds-error
message="{{'supervision-group-selector.select.type-of-order.error' | translate}}"></ds-error>
}
</div>
</div>
<div class="modal-body">
<div class="row">
<div class="control-group col-sm-12">
<label for="supervisionOrder">{{'supervision-group-selector.select.type-of-order.label' | translate}}</label>
<select name="supervisionOrder" id="supervisionOrder" class="form-control"
[(ngModel)]="selectedOrderType"
attr.aria-label="{{'supervision-group-selector.select.type-of-order.label' | translate}}">
<option value="EDITOR">{{'supervision-group-selector.select.type-of-order.option.editor' | translate}}</option>
<option value="OBSERVER">{{'supervision-group-selector.select.type-of-order.option.observer' | translate}}</option>
</select>
<ds-error *ngIf="isSubmitted && (!selectedOrderType || selectedOrderType === '')"
message="{{'supervision-group-selector.select.type-of-order.error' | translate}}"></ds-error>
</div>
</div>
<div class="row">
<div class="control-group col-sm-12">
<label for="supervisionGroup">{{'supervision-group-selector.select.group.label' | translate}}</label>
<ng-container class="mb-3">
<input id="supervisionGroup" class="form-control" type="text" [value]="selectedGroup ? dsoNameService.getName(selectedGroup) : ''" disabled>
<ds-error *ngIf="isSubmitted && !selectedGroup" message="{{'supervision-group-selector.select.group.error' | translate}}"></ds-error>
</ng-container>
<ds-eperson-group-list [isListOfEPerson]="false"
(select)="updateGroupObjectSelected($event)"></ds-eperson-group-list>
</div>
</div>
<!-- <div class="d-flex flex-row-reverse m-2"> -->
<div class="modal-footer">
<button class="btn btn-outline-secondary"
(click)="close()">
<i class="fas fa-times"></i> {{"supervision-group-selector.button.cancel" | translate}}
</button>
<button class="btn btn-primary save"
(click)="save()">
<i class="fas fa-save"></i> {{"supervision-group-selector.button.save" | translate}}
</button>
</div>
<div class="row">
<div class="control-group col-sm-12">
<label for="supervisionGroup" class="form-label">{{'supervision-group-selector.select.group.label' | translate}}</label>
<ng-container class="mb-3">
<input id="supervisionGroup" class="form-control" type="text" [value]="selectedGroup ? dsoNameService.getName(selectedGroup) : ''" disabled>
@if (isSubmitted && !selectedGroup) {
<ds-error message="{{'supervision-group-selector.select.group.error' | translate}}"></ds-error>
}
</ng-container>
<ds-eperson-group-list [isListOfEPerson]="false"
(select)="updateGroupObjectSelected($event)"></ds-eperson-group-list>
</div>
</div>
<!-- <div class="d-flex flex-row-reverse m-2"> -->
<div class="modal-footer">
<button class="btn btn-outline-secondary"
(click)="close()">
<i class="fas fa-times"></i> {{"supervision-group-selector.button.cancel" | translate}}
</button>
<button class="btn btn-primary save"
(click)="save()">
<i class="fas fa-save"></i> {{"supervision-group-selector.button.save" | translate}}
</button>
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { NgIf } from '@angular/common';
import {
Component,
EventEmitter,
@@ -34,7 +34,7 @@ import { ErrorComponent } from '../../../../../../shared/error/error.component';
styleUrls: ['./supervision-order-group-selector.component.scss'],
templateUrl: './supervision-order-group-selector.component.html',
standalone: true,
imports: [FormsModule, NgIf, ErrorComponent, EpersonGroupListComponent, TranslateModule],
imports: [FormsModule, ErrorComponent, EpersonGroupListComponent, TranslateModule],
})
export class SupervisionOrderGroupSelectorComponent {

Some files were not shown because too many files have changed in this diff Show More