diff --git a/config/config.example.yml b/config/config.example.yml
index 4fbc98fea2..8b010ba6ea 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -424,3 +424,12 @@ comcolSelectionSort:
# suggestion:
# - collectionId: 8f7df5ca-f9c2-47a4-81ec-8a6393d6e5af
# source: "openaire"
+
+
+# Search settings
+search:
+ # Settings to enable/disable or configure advanced search filters.
+ advancedFilters:
+ enabled: false
+ # List of filters to enable in "Advanced Search" dropdown
+ filter: [ 'title', 'author', 'subject', 'entityType' ]
diff --git a/cypress/e2e/collection-edit.cy.ts b/cypress/e2e/collection-edit.cy.ts
index 3e7ecf6141..63d873db3e 100644
--- a/cypress/e2e/collection-edit.cy.ts
+++ b/cypress/e2e/collection-edit.cy.ts
@@ -45,7 +45,7 @@ describe('Edit Collection > Content Source tab', () => {
cy.get('#externalSourceCheck').check();
// Wait for the source controls to appear
- cy.get('ds-collection-source-controls').should('be.visible');
+ // cy.get('ds-collection-source-controls').should('be.visible');
// Analyze entire page for accessibility issues
testA11y('ds-collection-source');
diff --git a/cypress/e2e/collection-statistics.cy.ts b/cypress/e2e/collection-statistics.cy.ts
index 43bf67ce51..a08f8cb198 100644
--- a/cypress/e2e/collection-statistics.cy.ts
+++ b/cypress/e2e/collection-statistics.cy.ts
@@ -6,7 +6,7 @@ describe('Collection Statistics Page', () => {
it('should load if you click on "Statistics" from a Collection page', () => {
cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')));
- cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
+ cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE);
});
diff --git a/cypress/e2e/community-statistics.cy.ts b/cypress/e2e/community-statistics.cy.ts
index ca306eff5c..6cafed0350 100644
--- a/cypress/e2e/community-statistics.cy.ts
+++ b/cypress/e2e/community-statistics.cy.ts
@@ -6,7 +6,7 @@ describe('Community Statistics Page', () => {
it('should load if you click on "Statistics" from a Community page', () => {
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
- cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
+ cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
});
diff --git a/cypress/e2e/homepage-statistics.cy.ts b/cypress/e2e/homepage-statistics.cy.ts
index ff7dbeb852..ece38686b9 100644
--- a/cypress/e2e/homepage-statistics.cy.ts
+++ b/cypress/e2e/homepage-statistics.cy.ts
@@ -5,7 +5,7 @@ import '../support/commands';
describe('Site Statistics Page', () => {
it('should load if you click on "Statistics" from homepage', () => {
cy.visit('/');
- cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
+ cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', '/statistics');
});
diff --git a/cypress/e2e/item-statistics.cy.ts b/cypress/e2e/item-statistics.cy.ts
index b856744cba..6caeacae8e 100644
--- a/cypress/e2e/item-statistics.cy.ts
+++ b/cypress/e2e/item-statistics.cy.ts
@@ -6,7 +6,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[title="Statistics"]').click();
+ cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
});
diff --git a/cypress/e2e/login-modal.cy.ts b/cypress/e2e/login-modal.cy.ts
index c56b98fd26..673041e9f3 100644
--- a/cypress/e2e/login-modal.cy.ts
+++ b/cypress/e2e/login-modal.cy.ts
@@ -3,31 +3,31 @@ import { testA11y } from 'cypress/support/utils';
const page = {
openLoginMenu() {
// Click the "Log In" dropdown menu in header
- cy.get('ds-themed-navbar [data-test="login-menu"]').click();
+ cy.get('ds-themed-header [data-test="login-menu"]').click();
},
openUserMenu() {
// Once logged in, click the User menu in header
- cy.get('ds-themed-navbar [data-test="user-menu"]').click();
+ cy.get('ds-themed-header [data-test="user-menu"]').click();
},
submitLoginAndPasswordByPressingButton(email, password) {
// Enter email
- cy.get('ds-themed-navbar [data-test="email"]').type(email);
+ cy.get('ds-themed-header [data-test="email"]').type(email);
// Enter password
- cy.get('ds-themed-navbar [data-test="password"]').type(password);
+ cy.get('ds-themed-header [data-test="password"]').type(password);
// Click login button
- cy.get('ds-themed-navbar [data-test="login-button"]').click();
+ cy.get('ds-themed-header [data-test="login-button"]').click();
},
submitLoginAndPasswordByPressingEnter(email, password) {
// In opened Login modal, fill out email & password, then click Enter
- cy.get('ds-themed-navbar [data-test="email"]').type(email);
- cy.get('ds-themed-navbar [data-test="password"]').type(password);
- cy.get('ds-themed-navbar [data-test="password"]').type('{enter}');
+ cy.get('ds-themed-header [data-test="email"]').type(email);
+ cy.get('ds-themed-header [data-test="password"]').type(password);
+ cy.get('ds-themed-header [data-test="password"]').type('{enter}');
},
submitLogoutByPressingButton() {
// This is the POST command that will actually log us out
cy.intercept('POST', '/server/api/authn/logout').as('logout');
// Click logout button
- cy.get('ds-themed-navbar [data-test="logout-button"]').click();
+ cy.get('ds-themed-header [data-test="logout-button"]').click();
// Wait until above POST command responds before continuing
// (This ensures next action waits until logout completes)
cy.wait('@logout');
@@ -102,10 +102,10 @@ describe('Login Modal', () => {
page.openLoginMenu();
// Registration link should be visible
- cy.get('ds-themed-navbar [data-test="register"]').should('be.visible');
+ cy.get('ds-themed-header [data-test="register"]').should('be.visible');
// Click registration link & you should go to registration page
- cy.get('ds-themed-navbar [data-test="register"]').click();
+ cy.get('ds-themed-header [data-test="register"]').click();
cy.location('pathname').should('eq', '/register');
cy.get('ds-register-email').should('exist');
@@ -119,10 +119,10 @@ describe('Login Modal', () => {
page.openLoginMenu();
// Forgot password link should be visible
- cy.get('ds-themed-navbar [data-test="forgot"]').should('be.visible');
+ cy.get('ds-themed-header [data-test="forgot"]').should('be.visible');
// Click link & you should go to Forgot Password page
- cy.get('ds-themed-navbar [data-test="forgot"]').click();
+ cy.get('ds-themed-header [data-test="forgot"]').click();
cy.location('pathname').should('eq', '/forgot');
cy.get('ds-forgot-email').should('exist');
diff --git a/cypress/e2e/search-navbar.cy.ts b/cypress/e2e/search-navbar.cy.ts
index 9dd93c7a2d..28a72bcc78 100644
--- a/cypress/e2e/search-navbar.cy.ts
+++ b/cypress/e2e/search-navbar.cy.ts
@@ -1,15 +1,15 @@
const page = {
fillOutQueryInNavBar(query) {
// Click the magnifying glass
- cy.get('ds-themed-navbar [data-test="header-search-icon"]').click();
+ cy.get('ds-themed-header [data-test="header-search-icon"]').click();
// Fill out a query in input that appears
- cy.get('ds-themed-navbar [data-test="header-search-box"]').type(query);
+ cy.get('ds-themed-header [data-test="header-search-box"]').type(query);
},
submitQueryByPressingEnter() {
- cy.get('ds-themed-navbar [data-test="header-search-box"]').type('{enter}');
+ cy.get('ds-themed-header [data-test="header-search-box"]').type('{enter}');
},
submitQueryByPressingIcon() {
- cy.get('ds-themed-navbar [data-test="header-search-icon"]').click();
+ cy.get('ds-themed-header [data-test="header-search-icon"]').click();
}
};
diff --git a/package.json b/package.json
index e5347742c8..abd25f4148 100644
--- a/package.json
+++ b/package.json
@@ -142,7 +142,7 @@
"@angular-eslint/eslint-plugin-template": "15.2.1",
"@angular-eslint/schematics": "15.2.1",
"@angular-eslint/template-parser": "15.2.1",
- "@angular/cli": "^15.2.6",
+ "@angular/cli": "^16.0.4",
"@angular/compiler-cli": "^15.2.8",
"@angular/language-service": "^15.2.8",
"@cypress/schematic": "^1.5.0",
@@ -170,7 +170,7 @@
"eslint": "^8.39.0",
"eslint-plugin-deprecation": "^1.4.1",
"eslint-plugin-import": "^2.27.5",
- "eslint-plugin-jsdoc": "^39.6.4",
+ "eslint-plugin-jsdoc": "^45.0.0",
"eslint-plugin-jsonc": "^2.6.0",
"eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-unused-imports": "^2.0.0",
diff --git a/src/app/admin/admin-ldn-services/admin-ldn-services-routing.module.ts b/src/app/admin/admin-ldn-services/admin-ldn-services-routing.module.ts
new file mode 100644
index 0000000000..c94083d3ba
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/admin-ldn-services-routing.module.ts
@@ -0,0 +1,47 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component';
+import { NavigationBreadcrumbResolver } from '../../core/breadcrumbs/navigation-breadcrumb.resolver';
+import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
+import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component';
+
+
+const moduleRoutes: Routes = [
+ {
+ path: '',
+ pathMatch: 'full',
+ component: LdnServicesOverviewComponent,
+ resolve: {breadcrumb: I18nBreadcrumbResolver},
+ data: {title: 'ldn-registered-services.title', breadcrumbKey: 'ldn-registered-services.new'},
+ },
+ {
+ path: 'new',
+ resolve: {breadcrumb: NavigationBreadcrumbResolver},
+ component: LdnServiceFormComponent,
+ data: {title: 'ldn-register-new-service.title', breadcrumbKey: 'ldn-register-new-service'}
+ },
+ {
+ path: 'edit/:serviceId',
+ resolve: {breadcrumb: NavigationBreadcrumbResolver},
+ component: LdnServiceFormComponent,
+ data: {title: 'ldn-edit-service.title', breadcrumbKey: 'ldn-edit-service'}
+ },
+];
+
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(moduleRoutes.map(route => {
+ return {...route, data: {
+ ...route.data,
+ relatedRoutes: moduleRoutes.filter(relatedRoute => relatedRoute.path !== route.path)
+ .map((relatedRoute) => {
+ return {path: relatedRoute.path, data: relatedRoute.data};
+ })
+ }};
+ }))
+ ]
+})
+export class AdminLdnServicesRoutingModule {
+
+}
diff --git a/src/app/admin/admin-ldn-services/admin-ldn-services.module.ts b/src/app/admin/admin-ldn-services/admin-ldn-services.module.ts
new file mode 100644
index 0000000000..45ec696cd3
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/admin-ldn-services.module.ts
@@ -0,0 +1,25 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { AdminLdnServicesRoutingModule } from './admin-ldn-services-routing.module';
+import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component';
+import { SharedModule } from '../../shared/shared.module';
+import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component';
+import { FormsModule } from '@angular/forms';
+import { LdnItemfiltersService } from './ldn-services-data/ldn-itemfilters-data.service';
+
+
+@NgModule({
+ imports: [
+ CommonModule,
+ SharedModule,
+ AdminLdnServicesRoutingModule,
+ FormsModule
+ ],
+ declarations: [
+ LdnServicesOverviewComponent,
+ LdnServiceFormComponent,
+ ],
+ providers: [LdnItemfiltersService]
+})
+export class AdminLdnServicesModule {
+}
diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html
new file mode 100644
index 0000000000..c11a41d887
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html
@@ -0,0 +1,310 @@
+
+
+
+
+
+
+ {{ 'service.overview.edit.body' | translate }}
+
+
+ {{ 'service.overview.create.body' | translate }}
+
+
+
+
+
+
diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.scss b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.scss
new file mode 100644
index 0000000000..afd5c80d1c
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.scss
@@ -0,0 +1,143 @@
+@import '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss';
+@import '../../../shared/form/form.component.scss';
+
+form {
+ font-size: 14px;
+ position: relative;
+}
+
+input,
+select {
+ max-width: 100%;
+ width: 100%;
+ padding: 8px;
+ font-size: 14px;
+}
+
+option:not(:first-child) {
+ font-weight: bold;
+}
+
+.trash-button {
+ width: 40px;
+ height: 40px;
+}
+
+textarea {
+ height: 200px;
+ resize: none;
+}
+
+.add-pattern-link {
+ color: #0048ff;
+ cursor: pointer;
+}
+
+.remove-pattern-link {
+ color: #e34949;
+ cursor: pointer;
+ margin-left: 10px;
+}
+
+.status-checkbox {
+ margin-top: 5px;
+}
+
+
+.invalid-field {
+ border: 1px solid red;
+ color: #000000;
+}
+
+.error-text {
+ color: red;
+ font-size: 0.8em;
+ margin-top: 5px;
+}
+
+.toggle-switch {
+ display: flex;
+ align-items: center;
+ opacity: 0.8;
+ position: relative;
+ width: 60px;
+ height: 30px;
+ background-color: #ccc;
+ border-radius: 15px;
+ cursor: pointer;
+ transition: background-color 0.3s;
+}
+
+.toggle-switch.checked {
+ background-color: #24cc9a;
+}
+
+.slider {
+ position: absolute;
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ background-color: #fff;
+ transition: transform 0.3s;
+}
+
+
+.toggle-switch .slider {
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ margin: 0 auto;
+}
+
+.toggle-switch.checked .slider {
+ transform: translateX(30px);
+}
+
+.toggle-switch-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-end;
+ margin-top: 10px;
+}
+
+.small-text {
+ font-size: 0.7em;
+ color: #888;
+}
+
+.toggle-switch {
+ cursor: pointer;
+ margin-top: 3px;
+ margin-right: 3px
+}
+
+.label-box {
+ margin-left: 11px;
+}
+
+.label-box-2 {
+ margin-left: 14px;
+}
+
+.label-box-3 {
+ margin-left: 5px;
+}
+
+.submission-form-footer {
+ border-radius: var(--bs-card-border-radius);
+ bottom: 0;
+ background-color: var(--bs-gray-400);
+ padding: calc(var(--bs-spacer) / 2);
+ z-index: calc(var(--ds-submission-footer-z-index) + 1);
+}
+
+.marked-for-deletion {
+ background-color: lighten($red, 30%);
+}
+
+.dropdown-menu-top, .scrollable-dropdown-menu {
+ z-index: var(--ds-submission-footer-z-index);
+}
+
+
diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.spec.ts b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.spec.ts
new file mode 100644
index 0000000000..e16ff49b7e
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.spec.ts
@@ -0,0 +1,241 @@
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+
+import {NgbDropdownModule, NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {LdnServiceFormComponent} from './ldn-service-form.component';
+import {ChangeDetectorRef, EventEmitter} from '@angular/core';
+import { FormArray, FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import {ActivatedRoute, Router} from '@angular/router';
+import {TranslateModule, TranslateService} from '@ngx-translate/core';
+import {PaginationService} from 'ngx-pagination';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {LdnItemfiltersService} from '../ldn-services-data/ldn-itemfilters-data.service';
+import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service';
+import {RouterStub} from '../../../shared/testing/router.stub';
+import {MockActivatedRoute} from '../../../shared/mocks/active-router.mock';
+import {NotificationsServiceStub} from '../../../shared/testing/notifications-service.stub';
+import { of as observableOf, of } from 'rxjs';
+import {RouteService} from '../../../core/services/route.service';
+import {provideMockStore} from '@ngrx/store/testing';
+import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
+import { By } from '@angular/platform-browser';
+
+describe('LdnServiceFormEditComponent', () => {
+ let component: LdnServiceFormComponent;
+ let fixture: ComponentFixture;
+
+ let ldnServicesService: LdnServicesService;
+ let ldnItemfiltersService: any;
+ let cdRefStub: any;
+ let modalService: any;
+ let activatedRoute: MockActivatedRoute;
+
+ const testId = '1234';
+ const routeParams = {
+ serviceId: testId,
+ };
+ const routeUrlSegments = [{path: 'path'}];
+ const formMockValue = {
+ 'id': '',
+ 'name': 'name',
+ 'description': 'description',
+ 'url': 'www.test.com',
+ 'ldnUrl': 'https://test.com',
+ 'lowerIp': '127.0.0.1',
+ 'upperIp': '100.100.100.100',
+ 'score': 1,
+ 'inboundPattern': '',
+ 'constraintPattern': '',
+ 'enabled': '',
+ 'type': 'ldnservice',
+ 'notifyServiceInboundPatterns': [
+ {
+ 'pattern': '',
+ 'patternLabel': 'Select a pattern',
+ 'constraint': '',
+ 'automatic': false
+ }
+ ]
+ };
+
+
+ const translateServiceStub = {
+ get: () => of('translated-text'),
+ instant: () => 'translated-text',
+ onLangChange: new EventEmitter(),
+ onTranslationChange: new EventEmitter(),
+ onDefaultLangChange: new EventEmitter()
+ };
+
+ beforeEach(async () => {
+ ldnServicesService = jasmine.createSpyObj('ldnServicesService', {
+ create: observableOf(null),
+ update: observableOf(null),
+ findById: createSuccessfulRemoteDataObject$({}),
+ });
+
+ ldnItemfiltersService = {
+ findAll: () => of(['item1', 'item2']),
+ };
+ cdRefStub = Object.assign({
+ detectChanges: () => fixture.detectChanges()
+ });
+ modalService = {
+ open: () => {/*comment*/
+ }
+ };
+
+
+ activatedRoute = new MockActivatedRoute(routeParams, routeUrlSegments);
+
+ await TestBed.configureTestingModule({
+ imports: [ReactiveFormsModule, TranslateModule.forRoot(), NgbDropdownModule],
+ declarations: [LdnServiceFormComponent],
+ providers: [
+ {provide: LdnServicesService, useValue: ldnServicesService},
+ {provide: LdnItemfiltersService, useValue: ldnItemfiltersService},
+ {provide: Router, useValue: new RouterStub()},
+ {provide: ActivatedRoute, useValue: activatedRoute},
+ {provide: ChangeDetectorRef, useValue: cdRefStub},
+ {provide: NgbModal, useValue: modalService},
+ {provide: NotificationsService, useValue: new NotificationsServiceStub()},
+ {provide: TranslateService, useValue: translateServiceStub},
+ {provide: PaginationService, useValue: {}},
+ FormBuilder,
+ RouteService,
+ provideMockStore({}),
+ ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(LdnServiceFormComponent);
+ component = fixture.componentInstance;
+ spyOn(component, 'filterPatternObjectsAndAssignLabel').and.callFake((a) => a);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ expect(component.formModel instanceof FormGroup).toBeTruthy();
+ });
+
+ it('should init properties correctly', fakeAsync(() => {
+ spyOn(component, 'fetchServiceData');
+ spyOn(component, 'setItemfilters');
+ component.ngOnInit();
+ tick(100);
+ expect((component as any).serviceId).toEqual(testId);
+ expect(component.isNewService).toBeFalsy();
+ expect(component.areControlsInitialized).toBeTruthy();
+ expect(component.formModel.controls.notifyServiceInboundPatterns).toBeDefined();
+ expect(component.fetchServiceData).toHaveBeenCalledWith(testId);
+ expect(component.setItemfilters).toHaveBeenCalled();
+ }));
+
+ it('should unsubscribe on destroy', () => {
+ spyOn((component as any).routeSubscription, 'unsubscribe');
+ component.ngOnDestroy();
+ expect((component as any).routeSubscription.unsubscribe).toHaveBeenCalled();
+ });
+
+ it('should handle create service with valid form', () => {
+ spyOn(component, 'fetchServiceData').and.callFake((a) => a);
+ component.formModel.addControl('notifyServiceInboundPatterns', (component as any).formBuilder.array([{pattern: 'patternValue'}]));
+ const nameInput = fixture.debugElement.query(By.css('#name'));
+ const descriptionInput = fixture.debugElement.query(By.css('#description'));
+ const urlInput = fixture.debugElement.query(By.css('#url'));
+ const scoreInput = fixture.debugElement.query(By.css('#score'));
+ const lowerIpInput = fixture.debugElement.query(By.css('#lowerIp'));
+ const upperIpInput = fixture.debugElement.query(By.css('#upperIp'));
+ const ldnUrlInput = fixture.debugElement.query(By.css('#ldnUrl'));
+ component.formModel.patchValue(formMockValue);
+
+ nameInput.nativeElement.value = 'testName';
+ descriptionInput.nativeElement.value = 'testDescription';
+ urlInput.nativeElement.value = 'tetsUrl.com';
+ ldnUrlInput.nativeElement.value = 'tetsLdnUrl.com';
+ scoreInput.nativeElement.value = 1;
+ lowerIpInput.nativeElement.value = '127.0.0.1';
+ upperIpInput.nativeElement.value = '127.0.0.1';
+
+ fixture.detectChanges();
+
+ expect(component.formModel.valid).toBeTruthy();
+ });
+
+ it('should handle create service with invalid form', () => {
+ const nameInput = fixture.debugElement.query(By.css('#name'));
+
+ nameInput.nativeElement.value = 'testName';
+ fixture.detectChanges();
+
+ expect(component.formModel.valid).toBeFalsy();
+ });
+
+ it('should not create service with invalid form', () => {
+ spyOn(component.formModel, 'markAllAsTouched');
+ spyOn(component, 'closeModal');
+ component.createService();
+
+ expect(component.formModel.markAllAsTouched).toHaveBeenCalled();
+ expect(component.closeModal).toHaveBeenCalled();
+ });
+
+ it('should create service with valid form', () => {
+ spyOn(component.formModel, 'markAllAsTouched');
+ spyOn(component, 'closeModal');
+ spyOn(component, 'checkPatterns').and.callFake(() => true);
+ component.formModel.addControl('notifyServiceInboundPatterns', (component as any).formBuilder.array([{pattern: 'patternValue'}]));
+ component.formModel.patchValue(formMockValue);
+ component.createService();
+
+ expect(component.formModel.markAllAsTouched).toHaveBeenCalled();
+ expect(component.closeModal).not.toHaveBeenCalled();
+ expect(ldnServicesService.create).toHaveBeenCalled();
+ });
+
+ it('should check patterns', () => {
+ const arrValid = new FormArray([
+ new FormGroup({
+ pattern: new FormControl('pattern')
+ }),
+ ]);
+
+ const arrInvalid = new FormArray([
+ new FormGroup({
+ pattern: new FormControl('')
+ }),
+ ]);
+
+ expect(component.checkPatterns(arrValid)).toBeTruthy();
+ expect(component.checkPatterns(arrInvalid)).toBeFalsy();
+ });
+
+ it('should fetch service data', () => {
+ component.fetchServiceData(testId);
+ expect(ldnServicesService.findById).toHaveBeenCalledWith(testId);
+ expect(component.filterPatternObjectsAndAssignLabel).toHaveBeenCalled();
+ expect((component as any).ldnService).toEqual({});
+ });
+
+ it('should generate patch operations', () => {
+ spyOn(component as any, 'createReplaceOperation');
+ spyOn(component as any, 'handlePatterns');
+ component.generatePatchOperations();
+ expect((component as any).createReplaceOperation).toHaveBeenCalledTimes(7);
+ expect((component as any).handlePatterns).toHaveBeenCalled();
+ });
+
+ it('should open modal on submit', () => {
+ spyOn(component, 'openConfirmModal');
+ component.onSubmit();
+ expect(component.openConfirmModal).toHaveBeenCalled();
+ });
+
+
+ it('should reset form and leave', () => {
+ spyOn(component as any, 'sendBack');
+
+ component.resetFormAndLeave();
+ expect((component as any).sendBack).toHaveBeenCalled();
+ });
+});
diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts
new file mode 100644
index 0000000000..0a08264bda
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts
@@ -0,0 +1,557 @@
+import {
+ ChangeDetectorRef,
+ Component,
+ OnDestroy,
+ OnInit,
+ TemplateRef,
+ ViewChild
+} from '@angular/core';
+import {FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type';
+import {ActivatedRoute, Router} from '@angular/router';
+import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service';
+import {notifyPatterns} from '../ldn-services-patterns/ldn-service-coar-patterns';
+import {animate, state, style, transition, trigger} from '@angular/animations';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {TranslateService} from '@ngx-translate/core';
+import {LdnService} from '../ldn-services-model/ldn-services.model';
+import {RemoteData} from 'src/app/core/data/remote-data';
+import {Operation} from 'fast-json-patch';
+import {getFirstCompletedRemoteData} from '../../../core/shared/operators';
+import {LdnItemfiltersService} from '../ldn-services-data/ldn-itemfilters-data.service';
+import {Itemfilter} from '../ldn-services-model/ldn-service-itemfilters';
+import {PaginatedList} from '../../../core/data/paginated-list.model';
+import {combineLatestWith, Observable, Subscription} from 'rxjs';
+import {PaginationService} from '../../../core/pagination/pagination.service';
+import {FindListOptions} from '../../../core/data/find-list-options.model';
+import {NotifyServicePattern} from '../ldn-services-model/ldn-service-patterns.model';
+import { IpV4Validator } from '../../../shared/utils/ipV4.validator';
+
+
+/**
+ * Component for editing LDN service through a form that allows to create or edit the properties of a service
+ */
+@Component({
+ selector: 'ds-ldn-service-form',
+ templateUrl: './ldn-service-form.component.html',
+ styleUrls: ['./ldn-service-form.component.scss'],
+ animations: [
+ trigger('toggleAnimation', [
+ state('true', style({})),
+ state('false', style({})),
+ transition('true <=> false', animate('300ms ease-in')),
+ ]),
+ ],
+})
+export class LdnServiceFormComponent implements OnInit, OnDestroy {
+ formModel: FormGroup;
+
+ @ViewChild('confirmModal', {static: true}) confirmModal: TemplateRef;
+ @ViewChild('resetFormModal', {static: true}) resetFormModal: TemplateRef;
+
+ public inboundPatterns: string[] = notifyPatterns;
+ public isNewService: boolean;
+ public areControlsInitialized: boolean;
+ public itemfiltersRD$: Observable>>;
+ public config: FindListOptions = Object.assign(new FindListOptions(), {
+ elementsPerPage: 20
+ });
+ public markedForDeletionInboundPattern: number[] = [];
+ public selectedInboundPatterns: string[];
+
+ protected serviceId: string;
+
+ private deletedInboundPatterns: number[] = [];
+ private modalRef: any;
+ private ldnService: LdnService;
+ private selectPatternDefaultLabeli18Key = 'ldn-service.form.label.placeholder.default-select';
+ private routeSubscription: Subscription;
+
+ constructor(
+ protected ldnServicesService: LdnServicesService,
+ private ldnItemfiltersService: LdnItemfiltersService,
+ private formBuilder: FormBuilder,
+ private router: Router,
+ private route: ActivatedRoute,
+ private cdRef: ChangeDetectorRef,
+ protected modalService: NgbModal,
+ private notificationService: NotificationsService,
+ private translateService: TranslateService,
+ protected paginationService: PaginationService
+ ) {
+
+ this.formModel = this.formBuilder.group({
+ id: [''],
+ name: ['', Validators.required],
+ description: [''],
+ url: ['', Validators.required],
+ ldnUrl: ['', Validators.required],
+ lowerIp: ['', [Validators.required, new IpV4Validator()]],
+ upperIp: ['', [Validators.required, new IpV4Validator()]],
+ score: ['', [Validators.required, Validators.pattern('^0*(\.[0-9]+)?$|^1(\.0+)?$')]], inboundPattern: [''],
+ constraintPattern: [''],
+ enabled: [''],
+ type: LDN_SERVICE.value,
+ });
+ }
+
+ ngOnInit(): void {
+ this.routeSubscription = this.route.params.pipe(
+ combineLatestWith(this.route.url)
+ ).subscribe(([params, segment]) => {
+ this.serviceId = params.serviceId;
+ this.isNewService = segment[0].path === 'new';
+ this.formModel.addControl('notifyServiceInboundPatterns', this.formBuilder.array([this.createInboundPatternFormGroup()]));
+ this.areControlsInitialized = true;
+ if (this.serviceId && !this.isNewService) {
+ this.fetchServiceData(this.serviceId);
+ }
+ });
+ this.setItemfilters();
+ }
+
+ ngOnDestroy(): void {
+ this.routeSubscription.unsubscribe();
+ }
+
+ /**
+ * Sets item filters using LDN item filters service
+ */
+ setItemfilters() {
+ this.itemfiltersRD$ = this.ldnItemfiltersService.findAll().pipe(
+ getFirstCompletedRemoteData());
+ }
+
+ /**
+ * Handles the creation of an LDN service by retrieving and validating form fields,
+ * and submitting the form data to the LDN services endpoint.
+ */
+ createService() {
+ this.formModel.markAllAsTouched();
+ const notifyServiceInboundPatterns = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
+ const hasInboundPattern = notifyServiceInboundPatterns?.length > 0 ? this.checkPatterns(notifyServiceInboundPatterns) : false;
+
+ if (this.formModel.invalid) {
+ this.closeModal();
+ return;
+ }
+
+ if (!hasInboundPattern) {
+ this.notificationService.warning(this.translateService.get('ldn-service-notification.created.warning.title'));
+ this.closeModal();
+ return;
+ }
+
+
+ this.formModel.value.notifyServiceInboundPatterns = this.formModel.value.notifyServiceInboundPatterns.map((pattern: {
+ pattern: string;
+ patternLabel: string,
+ constraintFormatted: string;
+ }) => {
+ const {patternLabel, ...rest} = pattern;
+ delete rest.constraintFormatted;
+ return rest;
+ });
+
+ const values = {...this.formModel.value, enabled: true};
+
+ const ldnServiceData = this.ldnServicesService.create(values);
+
+ ldnServiceData.pipe(
+ getFirstCompletedRemoteData()
+ ).subscribe((rd: RemoteData) => {
+ if (rd.hasSucceeded) {
+ this.notificationService.success(this.translateService.get('ldn-service-notification.created.success.title'),
+ this.translateService.get('ldn-service-notification.created.success.body'));
+ this.closeModal();
+ this.sendBack();
+ } else {
+ this.notificationService.error(this.translateService.get('ldn-service-notification.created.failure.title'),
+ this.translateService.get('ldn-service-notification.created.failure.body'));
+ this.closeModal();
+ }
+ });
+ }
+
+ /**
+ * Checks if at least one pattern in the specified form array has a value.
+ *
+ * @param {FormArray} formArray - The form array containing patterns to check.
+ * @returns {boolean} - True if at least one pattern has a value, otherwise false.
+ */
+ checkPatterns(formArray: FormArray): boolean {
+ for (let i = 0; i < formArray.length; i++) {
+ const pattern = formArray.at(i).get('pattern').value;
+ if (pattern) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Fetches LDN service data by ID and updates the form
+ * @param serviceId - The ID of the LDN service
+ */
+ fetchServiceData(serviceId: string): void {
+ this.ldnServicesService.findById(serviceId).pipe(
+ getFirstCompletedRemoteData()
+ ).subscribe(
+ (data: RemoteData) => {
+ if (data.hasSucceeded) {
+ this.ldnService = data.payload;
+ this.formModel.patchValue({
+ id: this.ldnService.id,
+ name: this.ldnService.name,
+ description: this.ldnService.description,
+ url: this.ldnService.url,
+ score: this.ldnService.score,
+ ldnUrl: this.ldnService.ldnUrl,
+ type: this.ldnService.type,
+ enabled: this.ldnService.enabled,
+ lowerIp: this.ldnService.lowerIp,
+ upperIp: this.ldnService.upperIp
+ });
+ this.filterPatternObjectsAndAssignLabel('notifyServiceInboundPatterns');
+ let notifyServiceInboundPatternsFormArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
+ notifyServiceInboundPatternsFormArray.controls.forEach(
+ control => {
+ const controlFormGroup = control as FormGroup;
+ const controlConstraint = controlFormGroup.get('constraint').value;
+ controlFormGroup.patchValue({
+ constraintFormatted: controlConstraint ? this.translateService.instant((controlConstraint as string) + '.label') : ''
+ });
+ }
+ );
+ }
+ },
+ );
+ }
+
+ /**
+ * Filters pattern objects, initializes form groups, assigns labels, and adds them to the specified form array so the correct string is shown in the dropdown..
+ * @param formArrayName - The name of the form array to be populated
+ */
+ filterPatternObjectsAndAssignLabel(formArrayName: string) {
+ const PatternsArray = this.formModel.get(formArrayName) as FormArray;
+ PatternsArray.clear();
+
+ let servicesToUse = this.ldnService.notifyServiceInboundPatterns;
+
+ servicesToUse.forEach((patternObj: NotifyServicePattern) => {
+ let patternFormGroup;
+ patternFormGroup = this.initializeInboundPatternFormGroup();
+ const newPatternObjWithLabel = Object.assign(new NotifyServicePattern(), {
+ ...patternObj,
+ patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternObj?.pattern + '.label')
+ });
+ patternFormGroup.patchValue(newPatternObjWithLabel);
+
+ PatternsArray.push(patternFormGroup);
+ this.cdRef.detectChanges();
+ });
+ }
+
+ /**
+ * Generates an array of patch operations based on form changes
+ * @returns Array of patch operations
+ */
+ generatePatchOperations(): any[] {
+ const patchOperations: any[] = [];
+
+ this.createReplaceOperation(patchOperations, 'name', '/name');
+ this.createReplaceOperation(patchOperations, 'description', '/description');
+ this.createReplaceOperation(patchOperations, 'ldnUrl', '/ldnurl');
+ this.createReplaceOperation(patchOperations, 'url', '/url');
+ this.createReplaceOperation(patchOperations, 'score', '/score');
+ this.createReplaceOperation(patchOperations, 'lowerIp', '/lowerIp');
+ this.createReplaceOperation(patchOperations, 'upperIp', '/upperIp');
+
+ this.handlePatterns(patchOperations, 'notifyServiceInboundPatterns');
+ this.deletedInboundPatterns.forEach(index => {
+ const removeOperation: Operation = {
+ op: 'remove',
+ path: `notifyServiceInboundPatterns[${index}]`
+ };
+ patchOperations.push(removeOperation);
+ });
+
+ return patchOperations;
+ }
+
+ /**
+ * Submits the form by opening the confirmation modal
+ */
+ onSubmit() {
+ this.openConfirmModal(this.confirmModal);
+ }
+
+ /**
+ * Adds a new inbound pattern form group to the array of inbound patterns in the form
+ */
+ addInboundPattern() {
+ const notifyServiceInboundPatternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
+ notifyServiceInboundPatternsArray.push(this.createInboundPatternFormGroup());
+ }
+
+ /**
+ * Selects an inbound pattern by updating its values based on the provided pattern value and index
+ * @param patternValue - The selected pattern value
+ * @param index - The index of the inbound pattern in the array
+ */
+ selectInboundPattern(patternValue: string, index: number): void {
+ const patternArray = (this.formModel.get('notifyServiceInboundPatterns') as FormArray);
+ patternArray.controls[index].patchValue({pattern: patternValue});
+ patternArray.controls[index].patchValue({patternLabel: this.translateService.instant('ldn-service.form.pattern.' + patternValue + '.label')});
+ }
+
+ /**
+ * Selects an inbound item filter by updating its value based on the provided filter value and index
+ * @param filterValue - The selected filter value
+ * @param index - The index of the inbound pattern in the array
+ */
+ selectInboundItemFilter(filterValue: string, index: number): void {
+ const filterArray = (this.formModel.get('notifyServiceInboundPatterns') as FormArray);
+ filterArray.controls[index].patchValue({
+ constraint: filterValue,
+ constraintFormatted: this.translateService.instant((filterValue !== '' ? filterValue : 'ldn.no-filter') + '.label')
+ });
+ filterArray.markAllAsTouched();
+ }
+
+ /**
+ * Toggles the automatic property of an inbound pattern at the specified index
+ * @param i - The index of the inbound pattern in the array
+ */
+ toggleAutomatic(i: number) {
+ const automaticControl = this.formModel.get(`notifyServiceInboundPatterns.${i}.automatic`);
+ if (automaticControl) {
+ automaticControl.markAsTouched();
+ automaticControl.setValue(!automaticControl.value);
+ }
+ }
+
+ /**
+ * Toggles the enabled status of the LDN service by sending a patch request
+ */
+ toggleEnabled() {
+ const newStatus = !this.formModel.get('enabled').value;
+
+ const patchOperation: Operation = {
+ op: 'replace',
+ path: '/enabled',
+ value: newStatus,
+ };
+
+ this.ldnServicesService.patch(this.ldnService, [patchOperation]).pipe(
+ getFirstCompletedRemoteData()
+ ).subscribe(
+ () => {
+ this.formModel.get('enabled').setValue(newStatus);
+ this.cdRef.detectChanges();
+ }
+ );
+ }
+
+ /**
+ * Closes the modal
+ */
+ closeModal() {
+ this.modalRef.close();
+ this.cdRef.detectChanges();
+ }
+
+ /**
+ * Opens a confirmation modal with the specified content
+ * @param content - The content to be displayed in the modal
+ */
+ openConfirmModal(content) {
+ this.modalRef = this.modalService.open(content);
+ }
+
+ /**
+ * Patches the LDN service by retrieving and sending patch operations geenrated in generatePatchOperations()
+ */
+ patchService() {
+ this.deleteMarkedInboundPatterns();
+
+ const patchOperations = this.generatePatchOperations();
+ this.formModel.markAllAsTouched();
+ // If the form is invalid, close the modal and return
+ if (this.formModel.invalid) {
+ this.closeModal();
+ return;
+ }
+
+ const notifyServiceInboundPatterns = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
+ const deletedInboundPatternsLength = this.deletedInboundPatterns.length;
+ // If no inbound patterns are specified, close the modal and return
+ // notify the user that no patterns are specified
+ if (notifyServiceInboundPatterns.length === deletedInboundPatternsLength) {
+ this.notificationService.warning(this.translateService.get('ldn-service-notification.created.warning.title'));
+ this.deletedInboundPatterns = [];
+ this.closeModal();
+ return;
+ }
+
+ this.ldnServicesService.patch(this.ldnService, patchOperations).pipe(
+ getFirstCompletedRemoteData()
+ ).subscribe(
+ (rd: RemoteData) => {
+ if (rd.hasSucceeded) {
+ this.closeModal();
+ this.sendBack();
+ this.notificationService.success(this.translateService.get('admin.registries.services-formats.modify.success.head'),
+ this.translateService.get('admin.registries.services-formats.modify.success.content'));
+ } else {
+ this.notificationService.error(this.translateService.get('admin.registries.services-formats.modify.failure.head'),
+ this.translateService.get('admin.registries.services-formats.modify.failure.content'));
+ this.closeModal();
+ }
+ });
+ }
+
+ /**
+ * Resets the form and navigates back to the LDN services page
+ */
+ resetFormAndLeave() {
+ this.sendBack();
+ }
+
+ /**
+ * Marks the specified inbound pattern for deletion
+ * @param index - The index of the inbound pattern in the array
+ */
+ markForInboundPatternDeletion(index: number) {
+ if (!this.markedForDeletionInboundPattern.includes(index)) {
+ this.markedForDeletionInboundPattern.push(index);
+ }
+ }
+
+ /**
+ * Unmarks the specified inbound pattern for deletion
+ * @param index - The index of the inbound pattern in the array
+ */
+ unmarkForInboundPatternDeletion(index: number) {
+ const i = this.markedForDeletionInboundPattern.indexOf(index);
+ if (i !== -1) {
+ this.markedForDeletionInboundPattern.splice(i, 1);
+ }
+ }
+
+ /**
+ * Deletes marked inbound patterns from the form model
+ */
+ deleteMarkedInboundPatterns() {
+ this.markedForDeletionInboundPattern.sort((a, b) => b - a);
+ const patternsArray = this.formModel.get('notifyServiceInboundPatterns') as FormArray;
+
+ for (const index of this.markedForDeletionInboundPattern) {
+ if (index >= 0 && index < patternsArray.length) {
+ const patternGroup = patternsArray.at(index) as FormGroup;
+ const patternValue = patternGroup.value;
+ if (patternValue.isNew) {
+ patternsArray.removeAt(index);
+ } else {
+ this.deletedInboundPatterns.push(index);
+ }
+ }
+ }
+
+ this.markedForDeletionInboundPattern = [];
+ }
+
+ /**
+ * Creates a replace operation and adds it to the patch operations if the form control is dirty
+ * @param patchOperations - The array to store patch operations
+ * @param formControlName - The name of the form control
+ * @param path - The JSON Patch path for the operation
+ */
+ private createReplaceOperation(patchOperations: any[], formControlName: string, path: string): void {
+ if (this.formModel.get(formControlName).dirty) {
+ patchOperations.push({
+ op: 'replace',
+ path,
+ value: this.formModel.get(formControlName).value.toString(),
+ });
+ }
+ }
+
+ /**
+ * Handles patterns in the form array, checking if an add or replace operations is required
+ * @param patchOperations - The array to store patch operations
+ * @param formArrayName - The name of the form array
+ */
+ private handlePatterns(patchOperations: any[], formArrayName: string): void {
+ const patternsArray = this.formModel.get(formArrayName) as FormArray;
+
+ for (let i = 0; i < patternsArray.length; i++) {
+ const patternGroup = patternsArray.at(i) as FormGroup;
+
+ const patternValue = patternGroup.value;
+ delete patternValue.constraintFormatted;
+ if (patternGroup.touched && patternGroup.valid) {
+ delete patternValue?.patternLabel;
+ if (patternValue.isNew) {
+ delete patternValue.isNew;
+ const addOperation = {
+ op: 'add',
+ path: `${formArrayName}/-`,
+ value: patternValue,
+ };
+ patchOperations.push(addOperation);
+ } else {
+ const replaceOperation = {
+ op: 'replace',
+ path: `${formArrayName}[${i}]`,
+ value: patternValue,
+ };
+ patchOperations.push(replaceOperation);
+ }
+ }
+ }
+ }
+
+ /**
+ * Navigates back to the LDN services page
+ */
+ private sendBack() {
+ this.router.navigateByUrl('admin/ldn/services');
+ }
+
+ /**
+ * Creates a form group for inbound patterns
+ * @returns The form group for inbound patterns
+ */
+ private createInboundPatternFormGroup(): FormGroup {
+ const inBoundFormGroup = {
+ pattern: '',
+ patternLabel: this.translateService.instant(this.selectPatternDefaultLabeli18Key),
+ constraint: '',
+ constraintFormatted: '',
+ automatic: false,
+ isNew: true
+ };
+
+ if (this.isNewService) {
+ delete inBoundFormGroup.isNew;
+ }
+
+ return this.formBuilder.group(inBoundFormGroup);
+ }
+
+ /**
+ * Initializes an existing form group for inbound patterns
+ * @returns The initialized form group for inbound patterns
+ */
+ private initializeInboundPatternFormGroup(): FormGroup {
+ return this.formBuilder.group({
+ pattern: '',
+ patternLabel: '',
+ constraint: '',
+ constraintFormatted: '',
+ automatic: '',
+ });
+ }
+}
diff --git a/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts b/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts
new file mode 100644
index 0000000000..d8534dde03
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts
@@ -0,0 +1,111 @@
+import {LdnService} from '../ldn-services-model/ldn-services.model';
+import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type';
+import {RemoteData} from '../../../core/data/remote-data';
+import {PaginatedList} from '../../../core/data/paginated-list.model';
+import {Observable, of} from 'rxjs';
+import {createSuccessfulRemoteDataObject$} from '../../../shared/remote-data.utils';
+
+export const mockLdnService: LdnService = {
+ uuid: '1',
+ enabled: false,
+ score: 0,
+ id: 1,
+ lowerIp: '192.0.2.146',
+ upperIp: '192.0.2.255',
+ name: 'Service Name',
+ description: 'Service Description',
+ url: 'Service URL',
+ ldnUrl: 'Service LDN URL',
+ notifyServiceInboundPatterns: [
+ {
+ pattern: 'patternA',
+ constraint: 'itemFilterA',
+ automatic: 'false',
+ },
+ {
+ pattern: 'patternB',
+ constraint: 'itemFilterB',
+ automatic: 'true',
+ },
+ ],
+ type: LDN_SERVICE,
+ _links: {
+ self: {
+ href: 'http://localhost/api/ldn/ldnservices/1'
+ },
+ },
+ get self(): string {
+ return '';
+ },
+};
+
+export const mockLdnServiceRD$ = createSuccessfulRemoteDataObject$(mockLdnService);
+
+
+export const mockLdnServices: LdnService[] = [{
+ uuid: '1',
+ enabled: false,
+ score: 0,
+ id: 1,
+ lowerIp: '192.0.2.146',
+ upperIp: '192.0.2.255',
+ name: 'Service Name',
+ description: 'Service Description',
+ url: 'Service URL',
+ ldnUrl: 'Service LDN URL',
+ notifyServiceInboundPatterns: [
+ {
+ pattern: 'patternA',
+ constraint: 'itemFilterA',
+ automatic: 'false',
+ },
+ {
+ pattern: 'patternB',
+ constraint: 'itemFilterB',
+ automatic: 'true',
+ },
+ ],
+ type: LDN_SERVICE,
+ _links: {
+ self: {
+ href: 'http://localhost/api/ldn/ldnservices/1'
+ },
+ },
+ get self(): string {
+ return '';
+ },
+}, {
+ uuid: '2',
+ enabled: false,
+ score: 0,
+ id: 2,
+ lowerIp: '192.0.2.146',
+ upperIp: '192.0.2.255',
+ name: 'Service Name',
+ description: 'Service Description',
+ url: 'Service URL',
+ ldnUrl: 'Service LDN URL',
+ notifyServiceInboundPatterns: [
+ {
+ pattern: 'patternA',
+ constraint: 'itemFilterA',
+ automatic: 'false',
+ },
+ {
+ pattern: 'patternB',
+ constraint: 'itemFilterB',
+ automatic: 'true',
+ },
+ ],
+ type: LDN_SERVICE,
+ _links: {
+ self: {
+ href: 'http://localhost/api/ldn/ldnservices/1'
+ },
+ },
+ get self(): string {
+ return '';
+ },
+}
+];
+export const mockLdnServicesRD$: Observable>> = of((mockLdnServices as unknown) as RemoteData>);
diff --git a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilter-data.service.spec.ts b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilter-data.service.spec.ts
new file mode 100644
index 0000000000..b5b0881727
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilter-data.service.spec.ts
@@ -0,0 +1,89 @@
+import { TestScheduler } from 'rxjs/testing';
+import { LdnItemfiltersService } from './ldn-itemfilters-data.service';
+import { RequestService } from '../../../core/data/request.service';
+import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
+import { ObjectCacheService } from '../../../core/cache/object-cache.service';
+import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
+import { NotificationsService } from '../../../shared/notifications/notifications.service';
+import { RequestEntry } from '../../../core/data/request-entry.model';
+import { RemoteData } from '../../../core/data/remote-data';
+import { RequestEntryState } from '../../../core/data/request-entry-state.model';
+import { cold, getTestScheduler } from 'jasmine-marbles';
+import { RestResponse } from '../../../core/cache/response.models';
+import { of } from 'rxjs';
+import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
+import { FindAllData } from '../../../core/data/base/find-all-data';
+import { testFindAllDataImplementation } from '../../../core/data/base/find-all-data.spec';
+
+describe('LdnItemfiltersService test', () => {
+ let scheduler: TestScheduler;
+ let service: LdnItemfiltersService;
+ let requestService: RequestService;
+ let rdbService: RemoteDataBuildService;
+ let objectCache: ObjectCacheService;
+ let halService: HALEndpointService;
+ let notificationsService: NotificationsService;
+ let responseCacheEntry: RequestEntry;
+
+ const endpointURL = `https://rest.api/rest/api/ldn/itemfilters`;
+ const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
+
+ const remoteDataMocks = {
+ Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200),
+ };
+
+ function initTestService() {
+ return new LdnItemfiltersService(
+ requestService,
+ rdbService,
+ objectCache,
+ halService,
+ notificationsService,
+ );
+ }
+
+ beforeEach(() => {
+ scheduler = getTestScheduler();
+
+ objectCache = {} as ObjectCacheService;
+ notificationsService = {} as NotificationsService;
+ responseCacheEntry = new RequestEntry();
+ responseCacheEntry.request = { href: 'https://rest.api/' } as any;
+ responseCacheEntry.response = new RestResponse(true, 200, 'Success');
+
+ requestService = jasmine.createSpyObj('requestService', {
+ generateRequestId: requestUUID,
+ send: true,
+ removeByHrefSubstring: {},
+ getByHref: of(responseCacheEntry),
+ getByUUID: of(responseCacheEntry),
+ });
+
+ halService = jasmine.createSpyObj('halService', {
+ getEndpoint: of(endpointURL)
+ });
+
+ rdbService = jasmine.createSpyObj('rdbService', {
+ buildSingle: createSuccessfulRemoteDataObject$({}, 500),
+ buildList: cold('a', { a: remoteDataMocks.Success })
+ });
+
+
+ service = initTestService();
+ });
+
+ describe('composition', () => {
+ const initFindAllService = () => new LdnItemfiltersService(null, null, null, null, null) as unknown as FindAllData;
+ testFindAllDataImplementation(initFindAllService);
+ });
+
+ describe('get endpoint', () => {
+ it('should retrieve correct endpoint', (done) => {
+ service.getEndpoint().subscribe(() => {
+ expect(halService.getEndpoint).toHaveBeenCalledWith('itemfilters');
+ done();
+ });
+ });
+ });
+
+});
diff --git a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service.ts b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service.ts
new file mode 100644
index 0000000000..15a7bcccda
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service.ts
@@ -0,0 +1,61 @@
+import {Injectable} from '@angular/core';
+import {dataService} from '../../../core/data/base/data-service.decorator';
+import {LDN_SERVICE_CONSTRAINT_FILTERS} from '../ldn-services-model/ldn-service.resource-type';
+import {IdentifiableDataService} from '../../../core/data/base/identifiable-data.service';
+import {FindAllData, FindAllDataImpl} from '../../../core/data/base/find-all-data';
+
+import {RequestService} from '../../../core/data/request.service';
+import {RemoteDataBuildService} from '../../../core/cache/builders/remote-data-build.service';
+import {ObjectCacheService} from '../../../core/cache/object-cache.service';
+import {HALEndpointService} from '../../../core/shared/hal-endpoint.service';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {FindListOptions} from '../../../core/data/find-list-options.model';
+import {FollowLinkConfig} from '../../../shared/utils/follow-link-config.model';
+import {Observable} from 'rxjs';
+import {RemoteData} from '../../../core/data/remote-data';
+import {Itemfilter} from '../ldn-services-model/ldn-service-itemfilters';
+import {PaginatedList} from '../../../core/data/paginated-list.model';
+
+
+/**
+ * A service responsible for fetching/sending data from/to the REST API on the itemfilters endpoint
+ */
+@Injectable()
+@dataService(LDN_SERVICE_CONSTRAINT_FILTERS)
+export class LdnItemfiltersService extends IdentifiableDataService implements FindAllData {
+ private findAllData: FindAllDataImpl;
+
+ constructor(
+ protected requestService: RequestService,
+ protected rdbService: RemoteDataBuildService,
+ protected objectCache: ObjectCacheService,
+ protected halService: HALEndpointService,
+ protected notificationsService: NotificationsService,
+ ) {
+ super('itemfilters', requestService, rdbService, objectCache, halService);
+
+ this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
+ }
+
+ /**
+ * Gets the endpoint URL for the itemfilters.
+ *
+ * @returns {string} - The endpoint URL.
+ */
+ getEndpoint() {
+ return this.halService.getEndpoint(this.linkPath);
+ }
+
+ /**
+ * Finds all itemfilters based on the provided options and link configurations.
+ *
+ * @param {FindListOptions} options - The options for finding a list of itemfilters.
+ * @param {boolean} useCachedVersionIfAvailable - Whether to use the cached version if available.
+ * @param {boolean} reRequestOnStale - Whether to re-request the data if it's stale.
+ * @param {...FollowLinkConfig[]} linksToFollow - Configurations for following specific links.
+ * @returns {Observable>>} - An observable of remote data containing a paginated list of itemfilters.
+ */
+ findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> {
+ return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
+ }
+}
diff --git a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.spec.ts b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.spec.ts
new file mode 100644
index 0000000000..9d17fc244c
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.spec.ts
@@ -0,0 +1,116 @@
+import { TestScheduler } from 'rxjs/testing';
+import { RequestService } from '../../../core/data/request.service';
+import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
+import { ObjectCacheService } from '../../../core/cache/object-cache.service';
+import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
+import { NotificationsService } from '../../../shared/notifications/notifications.service';
+import { RequestEntry } from '../../../core/data/request-entry.model';
+import { RemoteData } from '../../../core/data/remote-data';
+import { RequestEntryState } from '../../../core/data/request-entry-state.model';
+import { cold, getTestScheduler } from 'jasmine-marbles';
+import { RestResponse } from '../../../core/cache/response.models';
+import { of as observableOf } from 'rxjs';
+import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
+import { FindAllData } from '../../../core/data/base/find-all-data';
+import { testFindAllDataImplementation } from '../../../core/data/base/find-all-data.spec';
+import { LdnServicesService } from './ldn-services-data.service';
+import { testDeleteDataImplementation } from '../../../core/data/base/delete-data.spec';
+import { DeleteData } from '../../../core/data/base/delete-data';
+import { testSearchDataImplementation } from '../../../core/data/base/search-data.spec';
+import { SearchData } from '../../../core/data/base/search-data';
+import { testPatchDataImplementation } from '../../../core/data/base/patch-data.spec';
+import { PatchData } from '../../../core/data/base/patch-data';
+import { CreateData } from '../../../core/data/base/create-data';
+import { testCreateDataImplementation } from '../../../core/data/base/create-data.spec';
+import { FindListOptions } from '../../../core/data/find-list-options.model';
+import { RequestParam } from '../../../core/cache/models/request-param.model';
+import { mockLdnService } from '../ldn-service-serviceMock/ldnServicesRD$-mock';
+import { createPaginatedList } from '../../../shared/testing/utils.test';
+
+
+describe('LdnServicesService test', () => {
+ let scheduler: TestScheduler;
+ let service: LdnServicesService;
+ let requestService: RequestService;
+ let rdbService: RemoteDataBuildService;
+ let objectCache: ObjectCacheService;
+ let halService: HALEndpointService;
+ let notificationsService: NotificationsService;
+ let responseCacheEntry: RequestEntry;
+
+ const endpointURL = `https://rest.api/rest/api/ldn/ldnservices`;
+ const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
+
+ const remoteDataMocks = {
+ Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200),
+ };
+
+ function initTestService() {
+ return new LdnServicesService(
+ requestService,
+ rdbService,
+ objectCache,
+ halService,
+ notificationsService,
+ );
+ }
+
+ beforeEach(() => {
+ scheduler = getTestScheduler();
+
+ objectCache = {} as ObjectCacheService;
+ notificationsService = {} as NotificationsService;
+ responseCacheEntry = new RequestEntry();
+ responseCacheEntry.request = { href: 'https://rest.api/' } as any;
+ responseCacheEntry.response = new RestResponse(true, 200, 'Success');
+
+ requestService = jasmine.createSpyObj('requestService', {
+ generateRequestId: requestUUID,
+ send: true,
+ removeByHrefSubstring: {},
+ getByHref: observableOf(responseCacheEntry),
+ getByUUID: observableOf(responseCacheEntry),
+ });
+
+ halService = jasmine.createSpyObj('halService', {
+ getEndpoint: observableOf(endpointURL)
+ });
+
+ rdbService = jasmine.createSpyObj('rdbService', {
+ buildSingle: createSuccessfulRemoteDataObject$({}, 500),
+ buildList: cold('a', { a: remoteDataMocks.Success })
+ });
+
+
+ service = initTestService();
+ });
+
+ describe('composition', () => {
+ const initFindAllService = () => new LdnServicesService(null, null, null, null, null) as unknown as FindAllData;
+ const initDeleteService = () => new LdnServicesService(null, null, null, null, null) as unknown as DeleteData;
+ const initSearchService = () => new LdnServicesService(null, null, null, null, null) as unknown as SearchData;
+ const initPatchService = () => new LdnServicesService(null, null, null, null, null) as unknown as PatchData;
+ const initCreateService = () => new LdnServicesService(null, null, null, null, null) as unknown as CreateData;
+
+ testFindAllDataImplementation(initFindAllService);
+ testDeleteDataImplementation(initDeleteService);
+ testSearchDataImplementation(initSearchService);
+ testPatchDataImplementation(initPatchService);
+ testCreateDataImplementation(initCreateService);
+ });
+
+ describe('custom methods', () => {
+ it('should find service by inbound pattern', (done) => {
+ const params = [new RequestParam('pattern', 'testPattern')];
+ const findListOptions = Object.assign(new FindListOptions(), {}, {searchParams: params});
+ spyOn(service, 'searchBy').and.returnValue(observableOf(null));
+ spyOn((service as any).searchData, 'searchBy').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([mockLdnService])));
+
+ service.findByInboundPattern('testPattern').subscribe(() => {
+ expect(service.searchBy).toHaveBeenCalledWith('byInboundPattern', findListOptions, undefined, undefined );
+ done();
+ });
+ });
+ });
+
+});
diff --git a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts
new file mode 100644
index 0000000000..d1541e6bd8
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts
@@ -0,0 +1,217 @@
+import {Injectable} from '@angular/core';
+import {dataService} from '../../../core/data/base/data-service.decorator';
+import {LDN_SERVICE} from '../ldn-services-model/ldn-service.resource-type';
+import {IdentifiableDataService} from '../../../core/data/base/identifiable-data.service';
+import {FindAllData, FindAllDataImpl} from '../../../core/data/base/find-all-data';
+import {DeleteData, DeleteDataImpl} from '../../../core/data/base/delete-data';
+import {RequestService} from '../../../core/data/request.service';
+import {RemoteDataBuildService} from '../../../core/cache/builders/remote-data-build.service';
+import {ObjectCacheService} from '../../../core/cache/object-cache.service';
+import {HALEndpointService} from '../../../core/shared/hal-endpoint.service';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {FindListOptions} from '../../../core/data/find-list-options.model';
+import {FollowLinkConfig} from '../../../shared/utils/follow-link-config.model';
+import {Observable} from 'rxjs';
+import {RemoteData} from '../../../core/data/remote-data';
+import {PaginatedList} from '../../../core/data/paginated-list.model';
+import {NoContent} from '../../../core/shared/NoContent.model';
+import {map, take} from 'rxjs/operators';
+import {URLCombiner} from '../../../core/url-combiner/url-combiner';
+import {MultipartPostRequest} from '../../../core/data/request.models';
+import {RestRequest} from '../../../core/data/rest-request.model';
+
+
+import {LdnService} from '../ldn-services-model/ldn-services.model';
+
+import {PatchData, PatchDataImpl} from '../../../core/data/base/patch-data';
+import {ChangeAnalyzer} from '../../../core/data/change-analyzer';
+import {Operation} from 'fast-json-patch';
+import {RestRequestMethod} from '../../../core/data/rest-request-method';
+import {CreateData, CreateDataImpl} from '../../../core/data/base/create-data';
+import {LdnServiceConstrain} from '../ldn-services-model/ldn-service.constrain.model';
+import {SearchDataImpl} from '../../../core/data/base/search-data';
+import {RequestParam} from '../../../core/cache/models/request-param.model';
+
+/**
+ * Injectable service responsible for fetching/sending data from/to the REST API on the ldnservices endpoint.
+ *
+ * @export
+ * @class LdnServicesService
+ * @extends {IdentifiableDataService}
+ * @implements {FindAllData}
+ * @implements {DeleteData}
+ * @implements {PatchData}
+ * @implements {CreateData}
+ */
+@Injectable()
+@dataService(LDN_SERVICE)
+export class LdnServicesService extends IdentifiableDataService implements FindAllData, DeleteData, PatchData, CreateData {
+ createData: CreateDataImpl;
+ private findAllData: FindAllDataImpl;
+ private deleteData: DeleteDataImpl;
+ private patchData: PatchDataImpl;
+ private comparator: ChangeAnalyzer;
+ private searchData: SearchDataImpl;
+
+ private findByPatternEndpoint = 'byInboundPattern';
+
+ constructor(
+ protected requestService: RequestService,
+ protected rdbService: RemoteDataBuildService,
+ protected objectCache: ObjectCacheService,
+ protected halService: HALEndpointService,
+ protected notificationsService: NotificationsService,
+ ) {
+ super('ldnservices', requestService, rdbService, objectCache, halService);
+
+ this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
+ this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive);
+ this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint);
+ this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.comparator, this.responseMsToLive, this.constructIdEndpoint);
+ this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive);
+ }
+
+ /**
+ * Creates an LDN service by sending a POST request to the REST API.
+ *
+ * @param {LdnService} object - The LDN service object to be created.
+ * @param params Array with additional params to combine with query string
+ * @returns {Observable>} - Observable containing the result of the creation operation.
+ */
+ create(object: LdnService, ...params: RequestParam[]): Observable> {
+ return this.createData.create(object, ...params);
+ }
+
+ /**
+ * Updates an LDN service by applying a set of operations through a PATCH request to the REST API.
+ *
+ * @param {LdnService} object - The LDN service object to be updated.
+ * @param {Operation[]} operations - The patch operations to be applied.
+ * @returns {Observable>} - Observable containing the result of the update operation.
+ */
+ patch(object: LdnService, operations: Operation[]): Observable> {
+ return this.patchData.patch(object, operations);
+ }
+
+ /**
+ * Updates an LDN service by sending a PUT request to the REST API.
+ *
+ * @param {LdnService} object - The LDN service object to be updated.
+ * @returns {Observable>} - Observable containing the result of the update operation.
+ */
+ update(object: LdnService): Observable> {
+ return this.patchData.update(object);
+ }
+
+ /**
+ * Commits pending updates by sending a PATCH request to the REST API.
+ *
+ * @param {RestRequestMethod} [method] - The HTTP method to be used for the request.
+ */
+ commitUpdates(method?: RestRequestMethod): void {
+ return this.patchData.commitUpdates(method);
+ }
+
+ /**
+ * Creates a patch representing the changes made to the LDN service in the cache.
+ *
+ * @param {LdnService} object - The LDN service object for which to create the patch.
+ * @returns {Observable} - Observable containing the patch operations.
+ */
+ createPatchFromCache(object: LdnService): Observable {
+ return this.patchData.createPatchFromCache(object);
+ }
+
+ /**
+ * Retrieves all LDN services from the REST API based on the provided options.
+ *
+ * @param {FindListOptions} [options] - The options to be applied to the request.
+ * @param {boolean} [useCachedVersionIfAvailable] - Flag indicating whether to use cached data if available.
+ * @param {boolean} [reRequestOnStale] - Flag indicating whether to re-request data if it's stale.
+ * @param {...FollowLinkConfig[]} linksToFollow - Optional links to follow during the request.
+ * @returns {Observable>>} - Observable containing the result of the request.
+ */
+ findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> {
+ return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
+ }
+
+ /**
+ * Retrieves LDN services based on the inbound pattern from the REST API.
+ *
+ * @param {string} pattern - The inbound pattern to be used in the search.
+ * @param {FindListOptions} [options] - The options to be applied to the request.
+ * @param {boolean} [useCachedVersionIfAvailable] - Flag indicating whether to use cached data if available.
+ * @param {boolean} [reRequestOnStale] - Flag indicating whether to re-request data if it's stale.
+ * @param {...FollowLinkConfig[]} linksToFollow - Optional links to follow during the request.
+ * @returns {Observable>>} - Observable containing the result of the request.
+ */
+ findByInboundPattern(pattern: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> {
+ const params = [new RequestParam('pattern', pattern)];
+ const findListOptions = Object.assign(new FindListOptions(), options, {searchParams: params});
+ return this.searchBy(this.findByPatternEndpoint, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
+ }
+
+ /**
+ * Deletes an LDN service by sending a DELETE request to the REST API.
+ *
+ * @param {string} objectId - The ID of the LDN service to be deleted.
+ * @param {string[]} [copyVirtualMetadata] - Optional virtual metadata to be copied during the deletion.
+ * @returns {Observable>} - Observable containing the result of the deletion operation.
+ */
+ public delete(objectId: string, copyVirtualMetadata?: string[]): Observable> {
+ return this.deleteData.delete(objectId, copyVirtualMetadata);
+ }
+
+ /**
+ * Deletes an LDN service by its HATEOAS link.
+ *
+ * @param {string} href - The HATEOAS link of the LDN service to be deleted.
+ * @param {string[]} [copyVirtualMetadata] - Optional virtual metadata to be copied during the deletion.
+ * @returns {Observable>} - Observable containing the result of the deletion operation.
+ */
+ public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> {
+ return this.deleteData.deleteByHref(href, copyVirtualMetadata);
+ }
+
+
+ /**
+ * Make a new FindListRequest with given search method
+ *
+ * @param searchMethod The search method for the object
+ * @param options The [[FindListOptions]] object
+ * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
+ * no valid cached version. Defaults to true
+ * @param reRequestOnStale Whether or not the request should automatically be re-
+ * requested after the response becomes stale
+ * @param linksToFollow List of {@link FollowLinkConfig} that indicate which
+ * {@link HALLink}s should be automatically resolved
+ * @return {Observable>}
+ * Return an observable that emits response from the server
+ */
+ public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> {
+ return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
+ }
+
+ public invoke(serviceName: string, serviceId: string, parameters: LdnServiceConstrain[], files: File[]): Observable> {
+ const requestId = this.requestService.generateRequestId();
+ this.getBrowseEndpoint().pipe(
+ take(1),
+ map((endpoint: string) => new URLCombiner(endpoint, serviceName, 'processes', serviceId).toString()),
+ map((endpoint: string) => {
+ const body = this.getInvocationFormData(parameters, files);
+ return new MultipartPostRequest(requestId, endpoint, body);
+ })
+ ).subscribe((request: RestRequest) => this.requestService.send(request));
+
+ return this.rdbService.buildFromRequestUUID(requestId);
+ }
+
+ private getInvocationFormData(constrain: LdnServiceConstrain[], files: File[]): FormData {
+ const form: FormData = new FormData();
+ form.set('properties', JSON.stringify(constrain));
+ files.forEach((file: File) => {
+ form.append('file', file);
+ });
+ return form;
+ }
+}
diff --git a/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.html b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.html
new file mode 100644
index 0000000000..0aaa39bda2
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.html
@@ -0,0 +1,99 @@
+
+
+
{{ 'ldn-registered-services.title' | translate }}
+
+
+
+
+
0"
+ [collectionSize]="(ldnServicesRD$ | async)?.payload?.totalElements"
+ [hideGear]="true"
+ [hidePagerWhenSinglePage]="true"
+ [pageInfoState]="(ldnServicesRD$ | async)?.payload"
+ [paginationOptions]="pageConfig">
+
+
+
+
+ {{ 'service.overview.table.name' | translate }} |
+ {{ 'service.overview.table.description' | translate }} |
+ {{ 'service.overview.table.status' | translate }} |
+ {{ 'service.overview.table.actions' | translate }} |
+
+
+
+
+ {{ ldnService.name }} |
+
+
+
+
+ {{ ldnService.description }}
+
+
+
+ |
+
+
+ {{ ldnService.enabled ? ('ldn-service.overview.table.enabled' | translate) : ('ldn-service.overview.table.disabled' | translate) }}
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'service.overview.delete.body' | translate }}
+
+
+
+
+
+
+
+
+
diff --git a/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.scss b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.scss
new file mode 100644
index 0000000000..07377d63d5
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.scss
@@ -0,0 +1,29 @@
+.status-indicator {
+ padding: 2.5px 25px 2.5px 25px;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background-color 0.5s;
+}
+
+.status-enabled {
+ background-color: #daf7a6;
+ color: #4f5359;
+ font-size: 85%;
+ font-weight: bold;
+
+}
+
+.status-enabled:hover {
+ background-color: #faa0a0;
+}
+
+.status-disabled {
+ background-color: #faa0a0;
+ color: #4f5359;
+ font-size: 85%;
+ font-weight: bold;
+}
+
+.status-disabled:hover {
+ background-color: #daf7a6;
+}
diff --git a/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.spec.ts b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.spec.ts
new file mode 100644
index 0000000000..664edcb27d
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.spec.ts
@@ -0,0 +1,144 @@
+import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
+import {ChangeDetectorRef, EventEmitter} from '@angular/core';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {NotificationsServiceStub} from '../../../shared/testing/notifications-service.stub';
+import {TranslateModule, TranslateService} from '@ngx-translate/core';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {LdnServicesService} from '../ldn-services-data/ldn-services-data.service';
+import {PaginationService} from '../../../core/pagination/pagination.service';
+import {PaginationServiceStub} from '../../../shared/testing/pagination-service.stub';
+import {of} from 'rxjs';
+import {LdnService} from '../ldn-services-model/ldn-services.model';
+import {PaginatedList} from '../../../core/data/paginated-list.model';
+import {RemoteData} from '../../../core/data/remote-data';
+import {LdnServicesOverviewComponent} from './ldn-services-directory.component';
+import {createSuccessfulRemoteDataObject$} from '../../../shared/remote-data.utils';
+import {createPaginatedList} from '../../../shared/testing/utils.test';
+
+describe('LdnServicesOverviewComponent', () => {
+ let component: LdnServicesOverviewComponent;
+ let fixture: ComponentFixture;
+ let ldnServicesService;
+ let paginationService;
+ let modalService: NgbModal;
+ let notificationsService: NotificationsService;
+ let translateService: TranslateService;
+
+ const translateServiceStub = {
+ get: () => of('translated-text'),
+ onLangChange: new EventEmitter(),
+ onTranslationChange: new EventEmitter(),
+ onDefaultLangChange: new EventEmitter()
+ };
+
+ beforeEach(async () => {
+ paginationService = new PaginationServiceStub();
+ ldnServicesService = jasmine.createSpyObj('LdnServicesService', ['findAll', 'delete', 'patch']);
+ await TestBed.configureTestingModule({
+ imports: [TranslateModule.forRoot()],
+ declarations: [LdnServicesOverviewComponent],
+ providers: [
+ {
+ provide: LdnServicesService,
+ useValue: ldnServicesService
+ },
+ {provide: PaginationService, useValue: paginationService},
+ {
+ provide: NgbModal, useValue: {
+ open: () => { /*comment*/
+ }
+ }
+ },
+ {provide: ChangeDetectorRef, useValue: {}},
+ {provide: NotificationsService, useValue: NotificationsServiceStub},
+ {provide: TranslateService, useValue: translateServiceStub},
+ ]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(LdnServicesOverviewComponent);
+ component = fixture.componentInstance;
+ ldnServicesService = TestBed.inject(LdnServicesService);
+ paginationService = TestBed.inject(PaginationService);
+ modalService = TestBed.inject(NgbModal);
+ notificationsService = TestBed.inject(NotificationsService);
+ translateService = TestBed.inject(TranslateService);
+ component.modalRef = jasmine.createSpyObj({close: null});
+ component.isProcessingSub = jasmine.createSpyObj({unsubscribe: null});
+ component.ldnServicesRD$ = of({} as RemoteData>);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('ngOnInit', () => {
+ it('should call setLdnServices', fakeAsync(() => {
+ spyOn(component, 'setLdnServices').and.callThrough();
+ component.ngOnInit();
+ tick();
+ expect(component.setLdnServices).toHaveBeenCalled();
+ }));
+
+ it('should set ldnServicesRD$ with mock data', fakeAsync(() => {
+ spyOn(component, 'setLdnServices').and.callThrough();
+ const testData: LdnService[] = Object.assign([new LdnService()], [
+ {id: 1, name: 'Service 1', description: 'Description 1', enabled: true},
+ {id: 2, name: 'Service 2', description: 'Description 2', enabled: false},
+ {id: 3, name: 'Service 3', description: 'Description 3', enabled: true}]);
+
+ const mockLdnServicesRD = createPaginatedList(testData);
+ component.ldnServicesRD$ = createSuccessfulRemoteDataObject$(mockLdnServicesRD);
+ fixture.detectChanges();
+
+ const tableRows = fixture.debugElement.nativeElement.querySelectorAll('tbody tr');
+ expect(tableRows.length).toBe(testData.length);
+ const firstRowContent = tableRows[0].textContent;
+ expect(firstRowContent).toContain('Service 1');
+ expect(firstRowContent).toContain('Description 1');
+ }));
+ });
+
+ describe('ngOnDestroy', () => {
+ it('should call paginationService.clearPagination and unsubscribe', () => {
+ // spyOn(paginationService, 'clearPagination');
+ // spyOn(component.isProcessingSub, 'unsubscribe');
+ component.ngOnDestroy();
+ expect(paginationService.clearPagination).toHaveBeenCalledWith(component.pageConfig.id);
+ expect(component.isProcessingSub.unsubscribe).toHaveBeenCalled();
+ });
+ });
+
+ describe('openDeleteModal', () => {
+ it('should open delete modal', () => {
+ spyOn(modalService, 'open');
+ component.openDeleteModal(component.deleteModal);
+ expect(modalService.open).toHaveBeenCalledWith(component.deleteModal);
+ });
+ });
+
+ describe('closeModal', () => {
+ it('should close modal and detect changes', () => {
+ // spyOn(component.modalRef, 'close');
+ spyOn(component.cdRef, 'detectChanges');
+ component.closeModal();
+ expect(component.modalRef.close).toHaveBeenCalled();
+ expect(component.cdRef.detectChanges).toHaveBeenCalled();
+ });
+ });
+
+ describe('deleteSelected', () => {
+ it('should delete selected service and update data', fakeAsync(() => {
+ const serviceId = '123';
+ const mockRemoteData = { /* just an empty object to retrieve as as RemoteData> */};
+ spyOn(component, 'setLdnServices').and.callThrough();
+ const deleteSpy = ldnServicesService.delete.and.returnValue(of(mockRemoteData as RemoteData>));
+ component.selectedServiceId = serviceId;
+ component.deleteSelected(serviceId, ldnServicesService);
+ tick();
+ expect(deleteSpy).toHaveBeenCalledWith(serviceId);
+ }));
+ });
+});
diff --git a/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.ts b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.ts
new file mode 100644
index 0000000000..b36d102cb0
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-directory/ldn-services-directory.component.ts
@@ -0,0 +1,176 @@
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ OnDestroy,
+ OnInit,
+ TemplateRef,
+ ViewChild
+} from '@angular/core';
+import {Observable, Subscription} from 'rxjs';
+import {RemoteData} from '../../../core/data/remote-data';
+import {PaginatedList} from '../../../core/data/paginated-list.model';
+import {FindListOptions} from '../../../core/data/find-list-options.model';
+import {LdnService} from '../ldn-services-model/ldn-services.model';
+import {PaginationComponentOptions} from '../../../shared/pagination/pagination-component-options.model';
+import {map, switchMap} from 'rxjs/operators';
+import {LdnServicesService} from 'src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service';
+import {PaginationService} from 'src/app/core/pagination/pagination.service';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {hasValue} from '../../../shared/empty.util';
+import {Operation} from 'fast-json-patch';
+import {getFirstCompletedRemoteData} from '../../../core/shared/operators';
+import {NotificationsService} from '../../../shared/notifications/notifications.service';
+import {TranslateService} from '@ngx-translate/core';
+
+
+/**
+ * The `LdnServicesOverviewComponent` is a component that provides an overview of LDN (Linked Data Notifications) services.
+ * It displays a paginated list of LDN services, allows users to edit and delete services,
+ * toggle the status of each service directly form the page and allows for creation of new services redirecting the user on the creation/edit form
+ */
+@Component({
+ selector: 'ds-ldn-services-directory',
+ templateUrl: './ldn-services-directory.component.html',
+ styleUrls: ['./ldn-services-directory.component.scss'],
+ changeDetection: ChangeDetectionStrategy.Default
+})
+export class LdnServicesOverviewComponent implements OnInit, OnDestroy {
+
+ selectedServiceId: string | number | null = null;
+ servicesData: any[] = [];
+ @ViewChild('deleteModal', {static: true}) deleteModal: TemplateRef;
+ ldnServicesRD$: Observable>>;
+ config: FindListOptions = Object.assign(new FindListOptions(), {
+ elementsPerPage: 10
+ });
+ pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
+ id: 'po',
+ pageSize: 10
+ });
+ isProcessingSub: Subscription;
+ modalRef: any;
+
+
+ constructor(
+ protected ldnServicesService: LdnServicesService,
+ protected paginationService: PaginationService,
+ protected modalService: NgbModal,
+ public cdRef: ChangeDetectorRef,
+ private notificationService: NotificationsService,
+ private translateService: TranslateService,
+ ) {
+ }
+
+ ngOnInit(): void {
+ this.setLdnServices();
+ }
+
+ /**
+ * Sets up the LDN services by fetching and observing the paginated list of services.
+ */
+ setLdnServices() {
+ this.ldnServicesRD$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe(
+ switchMap((config) => this.ldnServicesService.findAll(config, false, false).pipe(
+ getFirstCompletedRemoteData()
+ ))
+ );
+ }
+
+ ngOnDestroy(): void {
+ this.paginationService.clearPagination(this.pageConfig.id);
+ if (hasValue(this.isProcessingSub)) {
+ this.isProcessingSub.unsubscribe();
+ }
+ }
+
+ /**
+ * Opens the delete confirmation modal.
+ *
+ * @param {any} content - The content of the modal.
+ */
+ openDeleteModal(content) {
+ this.modalRef = this.modalService.open(content);
+ }
+
+ /**
+ * Closes the currently open modal and triggers change detection.
+ */
+ closeModal() {
+ this.modalRef.close();
+ this.cdRef.detectChanges();
+ }
+
+ /**
+ * Sets the selected LDN service ID for deletion and opens the delete confirmation modal.
+ *
+ * @param {number} serviceId - The ID of the service to be deleted.
+ */
+ selectServiceToDelete(serviceId: number) {
+ this.selectedServiceId = serviceId;
+ this.openDeleteModal(this.deleteModal);
+ }
+
+ /**
+ * Deletes the selected LDN service.
+ *
+ * @param {string} serviceId - The ID of the service to be deleted.
+ * @param {LdnServicesService} ldnServicesService - The service for managing LDN services.
+ */
+ deleteSelected(serviceId: string, ldnServicesService: LdnServicesService): void {
+ if (this.selectedServiceId !== null) {
+ ldnServicesService.delete(serviceId).pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData) => {
+ if (rd.hasSucceeded) {
+ this.servicesData = this.servicesData.filter(service => service.id !== serviceId);
+ this.ldnServicesRD$ = this.ldnServicesRD$.pipe(
+ map((remoteData: RemoteData>) => {
+ if (remoteData.hasSucceeded) {
+ remoteData.payload.page = remoteData.payload.page.filter(service => service.id.toString() !== serviceId);
+ }
+ return remoteData;
+ })
+ );
+ this.cdRef.detectChanges();
+ this.closeModal();
+ this.notificationService.success(this.translateService.get('ldn-service-delete.notification.success.title'),
+ this.translateService.get('ldn-service-delete.notification.success.content'));
+ } else {
+ this.notificationService.error(this.translateService.get('ldn-service-delete.notification.error.title'),
+ this.translateService.get('ldn-service-delete.notification.error.content'));
+ this.cdRef.detectChanges();
+ }
+ });
+ }
+ }
+
+ /**
+ * Toggles the status (enabled/disabled) of an LDN service.
+ *
+ * @param {any} ldnService - The LDN service object.
+ * @param {LdnServicesService} ldnServicesService - The service for managing LDN services.
+ */
+ toggleStatus(ldnService: any, ldnServicesService: LdnServicesService): void {
+ const newStatus = !ldnService.enabled;
+ const originalStatus = ldnService.enabled;
+
+ const patchOperation: Operation = {
+ op: 'replace',
+ path: '/enabled',
+ value: newStatus,
+ };
+
+ ldnServicesService.patch(ldnService, [patchOperation]).pipe(getFirstCompletedRemoteData()).subscribe(
+ (rd: RemoteData) => {
+ if (rd.hasSucceeded) {
+ ldnService.enabled = newStatus;
+ this.notificationService.success(this.translateService.get('ldn-enable-service.notification.success.title'),
+ this.translateService.get('ldn-enable-service.notification.success.content'));
+ } else {
+ ldnService.enabled = originalStatus;
+ this.notificationService.error(this.translateService.get('ldn-enable-service.notification.error.title'),
+ this.translateService.get('ldn-enable-service.notification.error.content'));
+ }
+ }
+ );
+ }
+}
diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters.ts
new file mode 100644
index 0000000000..55b7ad8b98
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters.ts
@@ -0,0 +1,31 @@
+import {autoserialize, deserialize, inheritSerialization} from 'cerialize';
+import {LDN_SERVICE_CONSTRAINT_FILTER} from './ldn-service.resource-type';
+import {CacheableObject} from '../../../core/cache/cacheable-object.model';
+import {typedObject} from '../../../core/cache/builders/build-decorators';
+import {excludeFromEquals} from '../../../core/utilities/equals.decorators';
+import {ResourceType} from '../../../core/shared/resource-type';
+
+/** A single filter value and its properties. */
+@typedObject
+@inheritSerialization(CacheableObject)
+export class Itemfilter extends CacheableObject {
+ static type = LDN_SERVICE_CONSTRAINT_FILTER;
+
+ @excludeFromEquals
+ @autoserialize
+ type: ResourceType;
+
+ @autoserialize
+ id: string;
+
+ @deserialize
+ _links: {
+ self: {
+ href: string;
+ };
+ };
+
+ get self(): string {
+ return this._links.self.href;
+ }
+}
diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-patterns.model.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-patterns.model.ts
new file mode 100644
index 0000000000..295426ba87
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-patterns.model.ts
@@ -0,0 +1,13 @@
+import {autoserialize} from 'cerialize';
+
+/**
+ * A single notify service pattern and his properties
+ */
+export class NotifyServicePattern {
+ @autoserialize
+ pattern: string;
+ @autoserialize
+ constraint: string;
+ @autoserialize
+ automatic: string;
+}
diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-status.model.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-status.model.ts
new file mode 100644
index 0000000000..040e4d37b8
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service-status.model.ts
@@ -0,0 +1,8 @@
+/**
+ * List of services statuses
+ */
+export enum LdnServiceStatus {
+ UNKOWN,
+ DISABLED,
+ ENABLED,
+}
diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.constrain.model.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.constrain.model.ts
new file mode 100644
index 0000000000..5121e47f69
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.constrain.model.ts
@@ -0,0 +1,3 @@
+export class LdnServiceConstrain {
+ void: any;
+}
diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.resource-type.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.resource-type.ts
new file mode 100644
index 0000000000..4fb510c032
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-service.resource-type.ts
@@ -0,0 +1,12 @@
+/**
+ * The resource type for Ldn-Services
+ *
+ * Needs to be in a separate file to prevent circular
+ * dependencies in webpack.
+ */
+import {ResourceType} from '../../../core/shared/resource-type';
+
+export const LDN_SERVICE = new ResourceType('ldnservice');
+export const LDN_SERVICE_CONSTRAINT_FILTERS = new ResourceType('itemfilters');
+
+export const LDN_SERVICE_CONSTRAINT_FILTER = new ResourceType('itemfilter');
diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-services.model.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-services.model.ts
new file mode 100644
index 0000000000..9e803fbc01
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-services.model.ts
@@ -0,0 +1,72 @@
+import {ResourceType} from '../../../core/shared/resource-type';
+import {CacheableObject} from '../../../core/cache/cacheable-object.model';
+import {autoserialize, deserialize, deserializeAs, inheritSerialization} from 'cerialize';
+import {LDN_SERVICE} from './ldn-service.resource-type';
+import {excludeFromEquals} from '../../../core/utilities/equals.decorators';
+import {typedObject} from '../../../core/cache/builders/build-decorators';
+import {NotifyServicePattern} from './ldn-service-patterns.model';
+
+
+/**
+ * LDN Services bounded to each selected pattern, relation set in service creation
+ */
+
+export interface LdnServiceByPattern {
+ allowsMultipleRequests: boolean;
+ services: LdnService[];
+}
+
+/** An LdnService and its properties. */
+@typedObject
+@inheritSerialization(CacheableObject)
+export class LdnService extends CacheableObject {
+ static type = LDN_SERVICE;
+
+ @excludeFromEquals
+ @autoserialize
+ type: ResourceType;
+
+ @autoserialize
+ id: number;
+
+ @deserializeAs('id')
+ uuid: string;
+
+ @autoserialize
+ name: string;
+
+ @autoserialize
+ description: string;
+
+ @autoserialize
+ url: string;
+
+ @autoserialize
+ score: number;
+
+ @autoserialize
+ enabled: boolean;
+
+ @autoserialize
+ ldnUrl: string;
+
+ @autoserialize
+ lowerIp: string;
+
+ @autoserialize
+ upperIp: string;
+
+ @autoserialize
+ notifyServiceInboundPatterns?: NotifyServicePattern[];
+
+ @deserialize
+ _links: {
+ self: {
+ href: string;
+ };
+ };
+
+ get self(): string {
+ return this._links.self.href;
+ }
+}
diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/service-constrain-type.model.ts b/src/app/admin/admin-ldn-services/ldn-services-model/service-constrain-type.model.ts
new file mode 100644
index 0000000000..c734503d95
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-model/service-constrain-type.model.ts
@@ -0,0 +1,10 @@
+/**
+ * List of parameter types used for scripts
+ */
+export enum LdnServiceConstrainType {
+ STRING = 'String',
+ DATE = 'date',
+ BOOLEAN = 'boolean',
+ FILE = 'InputStream',
+ OUTPUT = 'OutputStream'
+}
diff --git a/src/app/admin/admin-ldn-services/ldn-services-patterns/ldn-service-coar-patterns.ts b/src/app/admin/admin-ldn-services/ldn-services-patterns/ldn-service-coar-patterns.ts
new file mode 100644
index 0000000000..faa7dc82d7
--- /dev/null
+++ b/src/app/admin/admin-ldn-services/ldn-services-patterns/ldn-service-coar-patterns.ts
@@ -0,0 +1,16 @@
+/**
+ * All available patterns for LDN service creation.
+ * They are used to populate a dropdown in the LDN service form creation
+ */
+
+export const notifyPatterns = [
+
+ 'request-endorsement',
+
+ 'request-ingest',
+
+ 'request-review',
+
+];
+
+
diff --git a/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts b/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts
index add9a504dd..f524cd56c2 100644
--- a/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts
+++ b/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts
@@ -4,7 +4,7 @@ import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/r
/**
* Interface for the route parameters.
*/
-export interface AdminNotificationsPublicationClaimPageParams {
+export interface NotificationsSuggestionTargetsPageParams {
pageId?: string;
pageSize?: number;
currentPage?: number;
@@ -14,7 +14,7 @@ export interface AdminNotificationsPublicationClaimPageParams {
* This class represents a resolver that retrieve the route data before the route is activated.
*/
@Injectable()
-export class AdminNotificationsPublicationClaimPageResolver implements Resolve {
+export class NotificationsSuggestionTargetsPageResolver implements Resolve {
/**
* Method for resolving the parameters in the current route.
@@ -22,7 +22,7 @@ export class AdminNotificationsPublicationClaimPageResolver implements Resolve {
- let component: AdminNotificationsPublicationClaimPageComponent;
- let fixture: ComponentFixture;
+describe('NotificationsSuggestionTargetsPageComponent', () => {
+ let component: NotificationsSuggestionTargetsPageComponent;
+ let fixture: ComponentFixture;
beforeEach(async(() => {
TestBed.configureTestingModule({
@@ -16,10 +18,10 @@ describe('AdminNotificationsPublicationClaimPageComponent', () => {
TranslateModule.forRoot()
],
declarations: [
- AdminNotificationsPublicationClaimPageComponent
+ NotificationsSuggestionTargetsPageComponent
],
providers: [
- AdminNotificationsPublicationClaimPageComponent
+ NotificationsSuggestionTargetsPageComponent
],
schemas: [NO_ERRORS_SCHEMA]
})
@@ -27,7 +29,7 @@ describe('AdminNotificationsPublicationClaimPageComponent', () => {
}));
beforeEach(() => {
- fixture = TestBed.createComponent(AdminNotificationsPublicationClaimPageComponent);
+ fixture = TestBed.createComponent(NotificationsSuggestionTargetsPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
diff --git a/src/app/admin/admin-notifications/admin-notifications-routing-paths.ts b/src/app/admin/admin-notifications/admin-notifications-routing-paths.ts
index f92a96d242..9fcabedd64 100644
--- a/src/app/admin/admin-notifications/admin-notifications-routing-paths.ts
+++ b/src/app/admin/admin-notifications/admin-notifications-routing-paths.ts
@@ -1,9 +1,7 @@
-import { URLCombiner } from '../../core/url-combiner/url-combiner';
-import { getNotificationsModuleRoute } from '../admin-routing-paths';
export const QUALITY_ASSURANCE_EDIT_PATH = 'quality-assurance';
export const PUBLICATION_CLAIMS_PATH = 'publication-claim';
-export function getQualityAssuranceRoute(id: string) {
- return new URLCombiner(getNotificationsModuleRoute(), QUALITY_ASSURANCE_EDIT_PATH, id).toString();
+export function getQualityAssuranceEditRoute() {
+ return `/${QUALITY_ASSURANCE_EDIT_PATH}`;
}
diff --git a/src/app/admin/admin-notifications/admin-notifications-routing.module.ts b/src/app/admin/admin-notifications/admin-notifications-routing.module.ts
index 07a98aa080..c82c91233e 100644
--- a/src/app/admin/admin-notifications/admin-notifications-routing.module.ts
+++ b/src/app/admin/admin-notifications/admin-notifications-routing.module.ts
@@ -6,19 +6,37 @@ import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.r
import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service';
import { PUBLICATION_CLAIMS_PATH } from './admin-notifications-routing-paths';
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component';
-import { AdminNotificationsPublicationClaimPageResolver } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service';
import { QUALITY_ASSURANCE_EDIT_PATH } from './admin-notifications-routing-paths';
-import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component';
-import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.component';
-import { AdminQualityAssuranceTopicsPageResolver } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page-resolver.service';
-import { AdminQualityAssuranceEventsPageResolver } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.resolver';
-import { AdminQualityAssuranceSourcePageComponent } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component';
-import { AdminQualityAssuranceSourcePageResolver } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page-resolver.service';
+import {
+ SiteAdministratorGuard
+} from '../../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import { QualityAssuranceBreadcrumbResolver } from '../../core/breadcrumbs/quality-assurance-breadcrumb.resolver';
import { QualityAssuranceBreadcrumbService } from '../../core/breadcrumbs/quality-assurance-breadcrumb.service';
+import {
+ QualityAssuranceEventsPageResolver
+} from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver';
+import {
+ AdminNotificationsPublicationClaimPageResolver
+} from '../../quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service';
+import {
+ QualityAssuranceTopicsPageComponent
+} from '../../quality-assurance-notifications-pages/quality-assurance-topics-page/quality-assurance-topics-page.component';
+import {
+ QualityAssuranceTopicsPageResolver
+} from '../../quality-assurance-notifications-pages/quality-assurance-topics-page/quality-assurance-topics-page-resolver.service';
+import {
+ QualityAssuranceSourcePageComponent
+} from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-page.component';
+import {
+ QualityAssuranceSourcePageResolver
+} from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-page-resolver.service';
import {
SourceDataResolver
-} from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-data.resolver';
+} from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-data.resolver';
+import {
+ QualityAssuranceEventsPageComponent
+} from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.component';
+
@NgModule({
imports: [
@@ -41,11 +59,11 @@ import {
{
canActivate: [ AuthenticatedGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`,
- component: AdminQualityAssuranceTopicsPageComponent,
+ component: QualityAssuranceTopicsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: QualityAssuranceBreadcrumbResolver,
- openaireQualityAssuranceTopicsParams: AdminQualityAssuranceTopicsPageResolver
+ openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver
},
data: {
title: 'admin.quality-assurance.page.title',
@@ -55,12 +73,27 @@ import {
},
{
canActivate: [ AuthenticatedGuard ],
- path: `${QUALITY_ASSURANCE_EDIT_PATH}`,
- component: AdminQualityAssuranceSourcePageComponent,
+ path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/target/:targetId`,
+ component: QualityAssuranceTopicsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: I18nBreadcrumbResolver,
- openaireQualityAssuranceSourceParams: AdminQualityAssuranceSourcePageResolver,
+ openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver
+ },
+ data: {
+ title: 'admin.quality-assurance.page.title',
+ breadcrumbKey: 'admin.quality-assurance',
+ showBreadcrumbsFluid: false
+ }
+ },
+ {
+ canActivate: [ SiteAdministratorGuard ],
+ path: `${QUALITY_ASSURANCE_EDIT_PATH}`,
+ component: QualityAssuranceSourcePageComponent,
+ pathMatch: 'full',
+ resolve: {
+ breadcrumb: I18nBreadcrumbResolver,
+ openaireQualityAssuranceSourceParams: QualityAssuranceSourcePageResolver,
sourceData: SourceDataResolver
},
data: {
@@ -72,11 +105,11 @@ import {
{
canActivate: [ AuthenticatedGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/:topicId`,
- component: AdminQualityAssuranceEventsPageComponent,
+ component: QualityAssuranceEventsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: QualityAssuranceBreadcrumbResolver,
- openaireQualityAssuranceEventsParams: AdminQualityAssuranceEventsPageResolver
+ openaireQualityAssuranceEventsParams: QualityAssuranceEventsPageResolver
},
data: {
title: 'admin.notifications.event.page.title',
@@ -91,10 +124,10 @@ import {
I18nBreadcrumbsService,
AdminNotificationsPublicationClaimPageResolver,
SourceDataResolver,
- AdminQualityAssuranceSourcePageResolver,
- AdminQualityAssuranceTopicsPageResolver,
- AdminQualityAssuranceEventsPageResolver,
- AdminQualityAssuranceSourcePageResolver,
+ QualityAssuranceSourcePageResolver,
+ QualityAssuranceTopicsPageResolver,
+ QualityAssuranceEventsPageResolver,
+ QualityAssuranceSourcePageResolver,
QualityAssuranceBreadcrumbResolver,
QualityAssuranceBreadcrumbService
]
diff --git a/src/app/admin/admin-notifications/admin-notifications.module.ts b/src/app/admin/admin-notifications/admin-notifications.module.ts
index d9efb4c288..ea670d222c 100644
--- a/src/app/admin/admin-notifications/admin-notifications.module.ts
+++ b/src/app/admin/admin-notifications/admin-notifications.module.ts
@@ -4,11 +4,14 @@ import { CoreModule } from '../../core/core.module';
import { SharedModule } from '../../shared/shared.module';
import { AdminNotificationsRoutingModule } from './admin-notifications-routing.module';
import { AdminNotificationsPublicationClaimPageComponent } from './admin-notifications-publication-claim-page/admin-notifications-publication-claim-page.component';
-import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component';
-import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.component';
-import { AdminQualityAssuranceSourcePageComponent } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component';
import { NotificationsModule } from '../../notifications/notifications.module';
+
+
+
+
+
+
@NgModule({
imports: [
CommonModule,
@@ -19,9 +22,6 @@ import { NotificationsModule } from '../../notifications/notifications.module';
],
declarations: [
AdminNotificationsPublicationClaimPageComponent,
- AdminQualityAssuranceTopicsPageComponent,
- AdminQualityAssuranceEventsPageComponent,
- AdminQualityAssuranceSourcePageComponent
],
entryComponents: []
})
diff --git a/src/app/admin/admin-notifications/admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component.spec.ts b/src/app/admin/admin-notifications/admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component.spec.ts
deleted file mode 100644
index 451c911c4c..0000000000
--- a/src/app/admin/admin-notifications/admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component.spec.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { NO_ERRORS_SCHEMA } from '@angular/core';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { AdminQualityAssuranceSourcePageComponent } from './admin-quality-assurance-source-page.component';
-
-describe('AdminQualityAssuranceSourcePageComponent', () => {
- let component: AdminQualityAssuranceSourcePageComponent;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- declarations: [ AdminQualityAssuranceSourcePageComponent ],
- schemas: [NO_ERRORS_SCHEMA]
- })
- .compileComponents();
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(AdminQualityAssuranceSourcePageComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create AdminQualityAssuranceSourcePageComponent', () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/src/app/admin/admin-notifications/admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component.ts b/src/app/admin/admin-notifications/admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component.ts
deleted file mode 100644
index 447e5a2e55..0000000000
--- a/src/app/admin/admin-notifications/admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Component } from '@angular/core';
-
-/**
- * Component for the page that show the QA sources.
- */
-@Component({
- selector: 'ds-admin-quality-assurance-source-page-component',
- templateUrl: './admin-quality-assurance-source-page.component.html',
-})
-export class AdminQualityAssuranceSourcePageComponent {}
diff --git a/src/app/admin/admin-notifications/admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component.spec.ts b/src/app/admin/admin-notifications/admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component.spec.ts
deleted file mode 100644
index a32f60f017..0000000000
--- a/src/app/admin/admin-notifications/admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component.spec.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { NO_ERRORS_SCHEMA } from '@angular/core';
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page.component';
-
-describe('AdminQualityAssuranceTopicsPageComponent', () => {
- let component: AdminQualityAssuranceTopicsPageComponent;
- let fixture: ComponentFixture;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [ AdminQualityAssuranceTopicsPageComponent ],
- schemas: [NO_ERRORS_SCHEMA]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(AdminQualityAssuranceTopicsPageComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create AdminQualityAssuranceTopicsPageComponent', () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/src/app/admin/admin-reports/admin-reports-routing.module.ts b/src/app/admin/admin-reports/admin-reports-routing.module.ts
new file mode 100644
index 0000000000..9022429502
--- /dev/null
+++ b/src/app/admin/admin-reports/admin-reports-routing.module.ts
@@ -0,0 +1,37 @@
+import { FilteredCollectionsComponent } from './filtered-collections/filtered-collections.component';
+import { FilteredItemsComponent } from './filtered-items/filtered-items.component';
+import { RouterModule } from '@angular/router';
+import { NgModule } from '@angular/core';
+import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
+
+@NgModule({
+ imports: [
+ RouterModule.forChild([
+ {
+ path: 'collections',
+ resolve: { breadcrumb: I18nBreadcrumbResolver },
+ data: {title: 'admin.reports.collections.title', breadcrumbKey: 'admin.reports.collections'},
+ children: [
+ {
+ path: '',
+ component: FilteredCollectionsComponent
+ }
+ ]
+ },
+ {
+ path: 'queries',
+ resolve: { breadcrumb: I18nBreadcrumbResolver },
+ data: {title: 'admin.reports.items.title', breadcrumbKey: 'admin.reports.items'},
+ children: [
+ {
+ path: '',
+ component: FilteredItemsComponent
+ }
+ ]
+ }
+ ])
+ ]
+})
+export class AdminReportsRoutingModule {
+
+}
diff --git a/src/app/admin/admin-reports/admin-reports.module.ts b/src/app/admin/admin-reports/admin-reports.module.ts
new file mode 100644
index 0000000000..70dfba8a07
--- /dev/null
+++ b/src/app/admin/admin-reports/admin-reports.module.ts
@@ -0,0 +1,28 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FilteredCollectionsComponent } from './filtered-collections/filtered-collections.component';
+import { RouterModule } from '@angular/router';
+import { SharedModule } from '../../shared/shared.module';
+import { FormModule } from '../../shared/form/form.module';
+import { FilteredItemsComponent } from './filtered-items/filtered-items.component';
+import { AdminReportsRoutingModule } from './admin-reports-routing.module';
+import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
+import { FiltersComponent } from './filters-section/filters-section.component';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ SharedModule,
+ RouterModule,
+ FormModule,
+ AdminReportsRoutingModule,
+ NgbAccordionModule
+ ],
+ declarations: [
+ FilteredCollectionsComponent,
+ FilteredItemsComponent,
+ FiltersComponent
+ ]
+})
+export class AdminReportsModule {
+}
diff --git a/src/app/admin/admin-reports/filtered-collections/filtered-collection.model.ts b/src/app/admin/admin-reports/filtered-collections/filtered-collection.model.ts
new file mode 100644
index 0000000000..a48b1e02fa
--- /dev/null
+++ b/src/app/admin/admin-reports/filtered-collections/filtered-collection.model.ts
@@ -0,0 +1,36 @@
+export class FilteredCollection {
+
+ public label: string;
+ public handle: string;
+ public communityLabel: string;
+ public communityHandle: string;
+ public nbTotalItems: number;
+ public values = {};
+ public allFiltersValue: number;
+
+ public clear() {
+ this.label = '';
+ this.handle = '';
+ this.communityLabel = '';
+ this.communityHandle = '';
+ this.nbTotalItems = 0;
+ this.values = {};
+ this.allFiltersValue = 0;
+ }
+
+ public deserialize(object: any) {
+ this.clear();
+ this.label = object.label;
+ this.handle = object.handle;
+ this.communityLabel = object.community_label;
+ this.communityHandle = object.community_handle;
+ this.nbTotalItems = object.nb_total_items;
+ let valuesPerFilter = object.values;
+ for (let filter in valuesPerFilter) {
+ if (valuesPerFilter.hasOwnProperty(filter)) {
+ this.values[filter] = valuesPerFilter[filter];
+ }
+ }
+ this.allFiltersValue = object.all_filters_value;
+ }
+}
diff --git a/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.html b/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.html
new file mode 100644
index 0000000000..5199a115a6
--- /dev/null
+++ b/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.html
@@ -0,0 +1,64 @@
+
diff --git a/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.scss b/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.scss
new file mode 100644
index 0000000000..73ce5275e5
--- /dev/null
+++ b/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.scss
@@ -0,0 +1,3 @@
+.num {
+ text-align: center;
+}
diff --git a/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.spec.ts b/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.spec.ts
new file mode 100644
index 0000000000..fe5dc612ca
--- /dev/null
+++ b/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.spec.ts
@@ -0,0 +1,83 @@
+import { waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
+import { TranslateLoaderMock } from 'src/app/shared/mocks/translate-loader.mock';
+import { FormBuilder } from '@angular/forms';
+import { FilteredCollectionsComponent } from './filtered-collections.component';
+import { DspaceRestService } from 'src/app/core/dspace-rest/dspace-rest.service';
+import { NgbAccordion, NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { of as observableOf } from 'rxjs';
+import { RawRestResponse } from 'src/app/core/dspace-rest/raw-rest-response.model';
+
+describe('FiltersComponent', () => {
+ let component: FilteredCollectionsComponent;
+ let fixture: ComponentFixture;
+ let formBuilder: FormBuilder;
+
+ const expected = {
+ payload: {
+ collections: [],
+ summary: {
+ label: 'Test'
+ }
+ },
+ statusCode: 200,
+ statusText: 'OK'
+ } as RawRestResponse;
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [FilteredCollectionsComponent],
+ imports: [
+ NgbAccordionModule,
+ TranslateModule.forRoot({
+ loader: {
+ provide: TranslateLoader,
+ useClass: TranslateLoaderMock
+ }
+ }),
+ HttpClientTestingModule
+ ],
+ providers: [
+ FormBuilder,
+ DspaceRestService
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ });
+ }));
+
+ beforeEach(waitForAsync(() => {
+ formBuilder = TestBed.inject(FormBuilder);
+
+ fixture = TestBed.createComponent(FilteredCollectionsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ }));
+
+ it('should create the component', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should be displaying the filters panel initially', () => {
+ let accordion: NgbAccordion = component.accordionComponent;
+ expect(accordion.isExpanded('filters')).toBeTrue();
+ });
+
+ describe('toggle', () => {
+ beforeEach(() => {
+ spyOn(component, 'getFilteredCollections').and.returnValue(observableOf(expected));
+ spyOn(component.results, 'deserialize');
+ spyOn(component.accordionComponent, 'expand').and.callThrough();
+ component.submit();
+ fixture.detectChanges();
+ });
+
+ it('should be displaying the collections panel after submitting', waitForAsync(() => {
+ fixture.whenStable().then(() => {
+ expect(component.accordionComponent.expand).toHaveBeenCalledWith('collections');
+ expect(component.accordionComponent.isExpanded('collections')).toBeTrue();
+ });
+ }));
+ });
+});
diff --git a/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.ts b/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.ts
new file mode 100644
index 0000000000..23fde05278
--- /dev/null
+++ b/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.ts
@@ -0,0 +1,70 @@
+import { Component, ViewChild } from '@angular/core';
+import { FormBuilder, FormGroup } from '@angular/forms';
+import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap';
+import { Observable } from 'rxjs';
+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';
+import { environment } from 'src/environments/environment';
+import { FiltersComponent } from '../filters-section/filters-section.component';
+import { FilteredCollections } from './filtered-collections.model';
+
+/**
+ * Component representing the Filtered Collections content report
+ */
+@Component({
+ selector: 'ds-report-filtered-collections',
+ templateUrl: './filtered-collections.component.html',
+ styleUrls: ['./filtered-collections.component.scss']
+})
+export class FilteredCollectionsComponent {
+
+ queryForm: FormGroup;
+ results: FilteredCollections = new FilteredCollections();
+ @ViewChild('acc') accordionComponent: NgbAccordion;
+
+ constructor(
+ private formBuilder: FormBuilder,
+ private restService: DspaceRestService) {}
+
+ ngOnInit() {
+ this.queryForm = this.formBuilder.group({
+ filters: FiltersComponent.formGroup(this.formBuilder)
+ });
+ }
+
+ filtersFormGroup(): FormGroup {
+ return this.queryForm.get('filters') as FormGroup;
+ }
+
+ getGroup(filterId: string): string {
+ return FiltersComponent.getGroup(filterId).id;
+ }
+
+ submit() {
+ this
+ .getFilteredCollections()
+ .subscribe(
+ response => {
+ this.results.deserialize(response.payload);
+ this.accordionComponent.expand('collections');
+ }
+ );
+ }
+
+ getFilteredCollections(): Observable {
+ let params = this.toQueryString();
+ if (params.length > 0) {
+ params = `?${params}`;
+ }
+ let scheme = environment.rest.ssl ? 'https' : 'http';
+ let urlRestApp = `${scheme}://${environment.rest.host}:${environment.rest.port}${environment.rest.nameSpace}`;
+ return this.restService.request(RestRequestMethod.GET, `${urlRestApp}/api/contentreport/filteredcollections${params}`);
+ }
+
+ private toQueryString(): string {
+ let params = FiltersComponent.toQueryString(this.queryForm.value.filters);
+ return params;
+ }
+
+}
diff --git a/src/app/admin/admin-reports/filtered-collections/filtered-collections.model.ts b/src/app/admin/admin-reports/filtered-collections/filtered-collections.model.ts
new file mode 100644
index 0000000000..6ea5a2fc80
--- /dev/null
+++ b/src/app/admin/admin-reports/filtered-collections/filtered-collections.model.ts
@@ -0,0 +1,26 @@
+import { FilteredCollection } from './filtered-collection.model';
+
+export class FilteredCollections {
+
+ public collections: Array = [];
+ public summary: FilteredCollection = new FilteredCollection();
+
+ public clear() {
+ this.collections.splice(0, this.collections.length);
+ this.summary.clear();
+ }
+
+ public deserialize(object: any) {
+ this.clear();
+ let summary = object.summary;
+ this.summary.deserialize(summary);
+ let collections = object.collections;
+ for (let i = 0; i < collections.length; i++) {
+ let collection = collections[i];
+ let coll = new FilteredCollection();
+ coll.deserialize(collection);
+ this.collections.push(coll);
+ }
+ }
+
+}
diff --git a/src/app/admin/admin-reports/filtered-items/filtered-items-model.ts b/src/app/admin/admin-reports/filtered-items/filtered-items-model.ts
new file mode 100644
index 0000000000..2c384fe39c
--- /dev/null
+++ b/src/app/admin/admin-reports/filtered-items/filtered-items-model.ts
@@ -0,0 +1,23 @@
+import { Item } from 'src/app/core/shared/item.model';
+
+export class FilteredItems {
+
+ public items: Item[] = [];
+ public itemCount: number;
+
+ public clear() {
+ this.items.splice(0, this.items.length);
+ }
+
+ public deserialize(object: any, offset: number = 0) {
+ this.clear();
+ this.itemCount = object.itemCount;
+ let items = object.items;
+ for (let i = 0; i < items.length; i++) {
+ let item = items[i];
+ item.index = this.items.length + offset + 1;
+ this.items.push(item);
+ }
+ }
+
+}
diff --git a/src/app/admin/admin-reports/filtered-items/filtered-items.component.html b/src/app/admin/admin-reports/filtered-items/filtered-items.component.html
new file mode 100644
index 0000000000..4b6679bdbc
--- /dev/null
+++ b/src/app/admin/admin-reports/filtered-items/filtered-items.component.html
@@ -0,0 +1,175 @@
+
diff --git a/src/app/admin/admin-reports/filtered-items/filtered-items.component.scss b/src/app/admin/admin-reports/filtered-items/filtered-items.component.scss
new file mode 100644
index 0000000000..73ce5275e5
--- /dev/null
+++ b/src/app/admin/admin-reports/filtered-items/filtered-items.component.scss
@@ -0,0 +1,3 @@
+.num {
+ text-align: center;
+}
diff --git a/src/app/suggestion-notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.scss b/src/app/admin/admin-reports/filtered-items/filtered-items.component.spec.ts
similarity index 100%
rename from src/app/suggestion-notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component.scss
rename to src/app/admin/admin-reports/filtered-items/filtered-items.component.spec.ts
diff --git a/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts b/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts
new file mode 100644
index 0000000000..7fbf90565d
--- /dev/null
+++ b/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts
@@ -0,0 +1,336 @@
+import { Component, ViewChild } from '@angular/core';
+import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms';
+import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap';
+import { TranslateService } from '@ngx-translate/core';
+import { 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 { MetadataFieldDataService } from 'src/app/core/data/metadata-field-data.service';
+import { MetadataSchemaDataService } from 'src/app/core/data/metadata-schema-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';
+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 { environment } from 'src/environments/environment';
+import { FiltersComponent } from '../filters-section/filters-section.component';
+import { FilteredItems } from './filtered-items-model';
+import { OptionVO } from './option-vo.model';
+import { PresetQuery } from './preset-query.model';
+import { QueryPredicate } from './query-predicate.model';
+
+/**
+ * Component representing the Filtered Items content report.
+ */
+@Component({
+ selector: 'ds-report-filtered-items',
+ templateUrl: './filtered-items.component.html',
+ styleUrls: ['./filtered-items.component.scss']
+})
+export class FilteredItemsComponent {
+
+ collections: OptionVO[];
+ presetQueries: PresetQuery[];
+ metadataFields: OptionVO[];
+ metadataFieldsWithAny: OptionVO[];
+ predicates: OptionVO[];
+ pageLimits: OptionVO[];
+
+ queryForm: FormGroup;
+ currentPage = 0;
+ results: FilteredItems = new FilteredItems();
+ results$: Observable- ;
+ @ViewChild('acc') accordionComponent: NgbAccordion;
+
+ constructor(
+ private communityService: CommunityDataService,
+ private collectionService: CollectionDataService,
+ private metadataSchemaService: MetadataSchemaDataService,
+ private metadataFieldService: MetadataFieldDataService,
+ private translateService: TranslateService,
+ private formBuilder: FormBuilder,
+ private restService: DspaceRestService) {}
+
+ ngOnInit() {
+ this.loadCollections();
+ this.loadPresetQueries();
+ this.loadMetadataFields();
+ this.loadPredicates();
+ this.loadPageLimits();
+
+ let formQueryPredicates: FormGroup[] = [
+ new QueryPredicate().toFormGroup(this.formBuilder)
+ ];
+
+ this.queryForm = this.formBuilder.group({
+ collections: this.formBuilder.control([''], []),
+ presetQuery: this.formBuilder.control('new', []),
+ queryPredicates: this.formBuilder.array(formQueryPredicates),
+ pageLimit: this.formBuilder.control('10', []),
+ filters: FiltersComponent.formGroup(this.formBuilder),
+ additionalFields: this.formBuilder.control([], [])
+ });
+ }
+
+ loadCollections(): void {
+ this.collections = [];
+ let wholeRepo$ = this.translateService.stream('admin.reports.items.wholeRepo');
+ this.collections.push(OptionVO.collectionLoc('', wholeRepo$));
+
+ this.communityService.findAll({ elementsPerPage: 10000, currentPage: 1 }).pipe(
+ getFirstSucceededRemoteListPayload()
+ ).subscribe(
+ (communitiesRest: Community[]) => {
+ communitiesRest.forEach(community => {
+ let commVO = OptionVO.collection(community.uuid, community.name, true);
+ this.collections.push(commVO);
+
+ this.collectionService.findByParent(community.uuid, { elementsPerPage: 10000, currentPage: 1 }).pipe(
+ getFirstSucceededRemoteListPayload()
+ ).subscribe(
+ (collectionsRest: Collection[]) => {
+ collectionsRest.filter(collection => collection.firstMetadataValue('dspace.entity.type') === 'Publication')
+ .forEach(collection => {
+ let collVO = OptionVO.collection(collection.uuid, '–' + collection.name);
+ this.collections.push(collVO);
+ });
+ }
+ );
+ });
+ }
+ );
+ }
+
+ loadPresetQueries(): void {
+ this.presetQueries = [
+ PresetQuery.of('new', 'admin.reports.items.preset.new', []),
+ PresetQuery.of('q1', 'admin.reports.items.preset.hasNoTitle', [
+ QueryPredicate.of('dc.title', QueryPredicate.DOES_NOT_EXIST)
+ ]),
+ PresetQuery.of('q2', 'admin.reports.items.preset.hasNoIdentifierUri', [
+ QueryPredicate.of('dc.identifier.uri', QueryPredicate.DOES_NOT_EXIST)
+ ]),
+ PresetQuery.of('q3', 'admin.reports.items.preset.hasCompoundSubject', [
+ QueryPredicate.of('dc.subject.*', QueryPredicate.LIKE, '%;%')
+ ]),
+ PresetQuery.of('q4', 'admin.reports.items.preset.hasCompoundAuthor', [
+ QueryPredicate.of('dc.contributor.author', QueryPredicate.LIKE, '% and %')
+ ]),
+ PresetQuery.of('q5', 'admin.reports.items.preset.hasCompoundCreator', [
+ QueryPredicate.of('dc.creator', QueryPredicate.LIKE, '% and %')
+ ]),
+ PresetQuery.of('q6', 'admin.reports.items.preset.hasUrlInDescription', [
+ QueryPredicate.of('dc.description', QueryPredicate.MATCHES, '^.*(http://|https://|mailto:).*$')
+ ]),
+ PresetQuery.of('q7', 'admin.reports.items.preset.hasFullTextInProvenance', [
+ QueryPredicate.of('dc.description.provenance', QueryPredicate.MATCHES, '^.*No\. of bitstreams(.|\r|\n|\r\n)*\.(PDF|pdf|DOC|doc|PPT|ppt|DOCX|docx|PPTX|pptx).*$')
+ ]),
+ PresetQuery.of('q8', 'admin.reports.items.preset.hasNonFullTextInProvenance', [
+ 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*$')
+ ]),
+ PresetQuery.of('q10', 'admin.reports.items.preset.hasUnbreakingDataInDescription', [
+ QueryPredicate.of('dc.description.*', QueryPredicate.MATCHES, '^.*[^\s]{50,}.*$')
+ ]),
+ PresetQuery.of('q12', 'admin.reports.items.preset.hasXmlEntityInMetadata', [
+ QueryPredicate.of('*', QueryPredicate.MATCHES, '^.*.*$')
+ ]),
+ PresetQuery.of('q13', 'admin.reports.items.preset.hasNonAsciiCharInMetadata', [
+ QueryPredicate.of('*', QueryPredicate.MATCHES, '^.*[^[:ascii:]].*$')
+ ])
+ ];
+ }
+
+ loadMetadataFields(): void {
+ this.metadataFields = [];
+ this.metadataFieldsWithAny = [];
+ let anyField$ = this.translateService.stream('admin.reports.items.anyField');
+ this.metadataFieldsWithAny.push(OptionVO.itemLoc('*', anyField$));
+ this.metadataSchemaService.findAll({ elementsPerPage: 10000, currentPage: 1 }).pipe(
+ getFirstSucceededRemoteListPayload()
+ ).subscribe(
+ (schemasRest: MetadataSchema[]) => {
+ schemasRest.forEach(schema => {
+ this.metadataFieldService.findBySchema(schema, { elementsPerPage: 10000, currentPage: 1 }).pipe(
+ getFirstSucceededRemoteListPayload()
+ ).subscribe(
+ (fieldsRest: MetadataField[]) => {
+ fieldsRest.forEach(field => {
+ let fieldName = schema.prefix + '.' + field.toString();
+ let fieldVO = OptionVO.item(fieldName, fieldName);
+ this.metadataFields.push(fieldVO);
+ this.metadataFieldsWithAny.push(fieldVO);
+ if (isEmpty(field.qualifier)) {
+ fieldName = schema.prefix + '.' + field.element + '.*';
+ fieldVO = OptionVO.item(fieldName, fieldName);
+ this.metadataFieldsWithAny.push(fieldVO);
+ }
+ });
+ }
+ );
+ });
+ }
+ );
+ }
+
+ loadPredicates(): void {
+ this.predicates = [
+ OptionVO.item(QueryPredicate.EXISTS, 'admin.reports.items.predicate.exists'),
+ OptionVO.item(QueryPredicate.DOES_NOT_EXIST, 'admin.reports.items.predicate.doesNotExist'),
+ OptionVO.item(QueryPredicate.EQUALS, 'admin.reports.items.predicate.equals'),
+ OptionVO.item(QueryPredicate.DOES_NOT_EQUAL, 'admin.reports.items.predicate.doesNotEqual'),
+ OptionVO.item(QueryPredicate.LIKE, 'admin.reports.items.predicate.like'),
+ OptionVO.item(QueryPredicate.NOT_LIKE, 'admin.reports.items.predicate.notLike'),
+ OptionVO.item(QueryPredicate.CONTAINS, 'admin.reports.items.predicate.contains'),
+ OptionVO.item(QueryPredicate.DOES_NOT_CONTAIN, 'admin.reports.items.predicate.doesNotContain'),
+ OptionVO.item(QueryPredicate.MATCHES, 'admin.reports.items.predicate.matches'),
+ OptionVO.item(QueryPredicate.DOES_NOT_MATCH, 'admin.reports.items.predicate.doesNotMatch')
+ ];
+ }
+
+ loadPageLimits(): void {
+ this.pageLimits = [
+ OptionVO.item('10', '10'),
+ OptionVO.item('25', '25'),
+ OptionVO.item('50', '50'),
+ OptionVO.item('100', '100')
+ ];
+ }
+
+ queryPredicatesArray(): FormArray {
+ return (this.queryForm.get('queryPredicates') as FormArray);
+ }
+
+ addQueryPredicate(newItem: FormGroup = new QueryPredicate().toFormGroup(this.formBuilder)) {
+ this.queryPredicatesArray().push(newItem);
+ }
+
+ deleteQueryPredicateDisabled(): boolean {
+ return this.queryPredicatesArray().length < 2;
+ }
+
+ deleteQueryPredicate(index: number, nbToDelete: number = 1) {
+ if (index > -1) {
+ this.queryPredicatesArray().removeAt(index);
+ }
+ }
+
+ setPresetQuery() {
+ let queryField = this.queryForm.controls.presetQuery as FormControl;
+ let value = queryField.value;
+ let query = this.presetQueries.find(q => q.id === value);
+ if (query !== undefined) {
+ this.queryPredicatesArray().clear();
+ query.predicates
+ .map(qp => qp.toFormGroup(this.formBuilder))
+ .forEach(qp => this.addQueryPredicate(qp));
+ if (query.predicates.length === 0) {
+ this.addQueryPredicate(new QueryPredicate().toFormGroup(this.formBuilder));
+ }
+ }
+ }
+
+ filtersFormGroup(): FormGroup {
+ return this.queryForm.get('filters') as FormGroup;
+ }
+
+ private pageSize() {
+ let form = this.queryForm.value;
+ return form.pageLimit;
+ }
+
+ canNavigatePrevious(): boolean {
+ return this.currentPage > 0;
+ }
+
+ prevPage() {
+ if (this.canNavigatePrevious()) {
+ this.currentPage--;
+ this.resubmit();
+ }
+ }
+
+ pageCount(): number {
+ let total = this.results.itemCount || 0;
+ return Math.ceil(total / this.pageSize());
+ }
+
+ canNavigateNext(): boolean {
+ return this.currentPage + 1 < this.pageCount();
+ }
+
+ nextPage() {
+ if (this.canNavigateNext()) {
+ this.currentPage++;
+ this.resubmit();
+ }
+ }
+
+ submit() {
+ this.accordionComponent.expand('itemResults');
+ this.currentPage = 0;
+ this.resubmit();
+ }
+
+ resubmit() {
+ this.results$ = this
+ .getFilteredItems()
+ .pipe(
+ map(response => {
+ let offset = this.currentPage * this.pageSize();
+ this.results.deserialize(response.payload, offset);
+ return this.results.items;
+ })
+ );
+ }
+
+ getFilteredItems(): Observable {
+ let params = this.toQueryString();
+ if (params.length > 0) {
+ params = `?${params}`;
+ }
+ let scheme = environment.rest.ssl ? 'https' : 'http';
+ let urlRestApp = `${scheme}://${environment.rest.host}:${environment.rest.port}${environment.rest.nameSpace}`;
+ return this.restService.request(RestRequestMethod.GET, `${urlRestApp}/api/contentreport/filtereditems${params}`);
+ }
+
+ private toQueryString(): string {
+ let params = `pageNumber=${this.currentPage}&pageLimit=${this.pageSize()}`;
+
+ let colls = this.queryForm.value.collections;
+ for (let i = 0; i < colls.length; i++) {
+ params += `&collections=${colls[i]}`;
+ }
+
+ let 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}`;
+ }
+ }
+
+ let filters = FiltersComponent.toQueryString(this.queryForm.value.filters);
+ if (filters.length > 0) {
+ params += `&${filters}`;
+ }
+
+ let addFlds = this.queryForm.value.additionalFields;
+ for (let i = 0; i < addFlds.length; i++) {
+ params += `&additionalFields=${addFlds[i]}`;
+ }
+
+ return params;
+ }
+
+}
diff --git a/src/app/admin/admin-reports/filtered-items/option-vo.model.ts b/src/app/admin/admin-reports/filtered-items/option-vo.model.ts
new file mode 100644
index 0000000000..0aee34d070
--- /dev/null
+++ b/src/app/admin/admin-reports/filtered-items/option-vo.model.ts
@@ -0,0 +1,50 @@
+import { Observable } from 'rxjs';
+
+/**
+ * Component representing an option in each selectable list of values
+ * used in the Filtered Items report query interface
+ */
+export class OptionVO {
+
+ id: string;
+ name$: Observable;
+ disabled = false;
+
+ static collection(id: string, name: string, disabled: boolean = false): OptionVO {
+ let opt = new OptionVO();
+ opt.id = id;
+ opt.name$ = OptionVO.toObservable(name);
+ opt.disabled = disabled;
+ return opt;
+ }
+
+ static collectionLoc(id: string, name$: Observable, disabled: boolean = false): OptionVO {
+ let opt = new OptionVO();
+ opt.id = id;
+ opt.name$ = name$;
+ opt.disabled = disabled;
+ return opt;
+ }
+
+ static item(id: string, name: string): OptionVO {
+ let opt = new OptionVO();
+ opt.id = id;
+ opt.name$ = OptionVO.toObservable(name);
+ return opt;
+ }
+
+ static itemLoc(id: string, name$: Observable): OptionVO {
+ let opt = new OptionVO();
+ opt.id = id;
+ opt.name$ = name$;
+ return opt;
+ }
+
+ private static toObservable(value: T): Observable {
+ return new Observable(subscriber => {
+ subscriber.next(value);
+ subscriber.complete();
+ });
+
+ }
+}
diff --git a/src/app/admin/admin-reports/filtered-items/preset-query.model.ts b/src/app/admin/admin-reports/filtered-items/preset-query.model.ts
new file mode 100644
index 0000000000..73522f02cf
--- /dev/null
+++ b/src/app/admin/admin-reports/filtered-items/preset-query.model.ts
@@ -0,0 +1,17 @@
+import { QueryPredicate } from './query-predicate.model';
+
+export class PresetQuery {
+
+ id: string;
+ label: string;
+ predicates: QueryPredicate[];
+
+ static of(id: string, label: string, predicates: QueryPredicate[]) {
+ let query = new PresetQuery();
+ query.id = id;
+ query.label = label;
+ query.predicates = predicates;
+ return query;
+ }
+
+}
diff --git a/src/app/admin/admin-reports/filtered-items/query-predicate.model.ts b/src/app/admin/admin-reports/filtered-items/query-predicate.model.ts
new file mode 100644
index 0000000000..c5f323ed2c
--- /dev/null
+++ b/src/app/admin/admin-reports/filtered-items/query-predicate.model.ts
@@ -0,0 +1,36 @@
+import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
+
+export class QueryPredicate {
+
+ static EXISTS = 'exists';
+ static DOES_NOT_EXIST = 'doesnt_exist';
+ static EQUALS = 'equals';
+ static DOES_NOT_EQUAL = 'not_equals';
+ static LIKE = 'like';
+ static NOT_LIKE = 'not_like';
+ static CONTAINS = 'contains';
+ static DOES_NOT_CONTAIN = 'doesnt_contain';
+ static MATCHES = 'matches';
+ static DOES_NOT_MATCH = 'doesnt_match';
+
+ field = '*';
+ operator: string;
+ value: string;
+
+ static of(field: string, operator: string, value: string = '') {
+ let pred = new QueryPredicate();
+ pred.field = field;
+ pred.operator = operator;
+ pred.value = value;
+ return pred;
+ }
+
+ toFormGroup(formBuilder: FormBuilder): FormGroup {
+ return formBuilder.group({
+ field: new FormControl(this.field),
+ operator: new FormControl(this.operator),
+ value: new FormControl(this.value)
+ });
+ }
+
+}
diff --git a/src/app/admin/admin-reports/filters-section/filter-group.model.ts b/src/app/admin/admin-reports/filters-section/filter-group.model.ts
new file mode 100644
index 0000000000..975b43a986
--- /dev/null
+++ b/src/app/admin/admin-reports/filters-section/filter-group.model.ts
@@ -0,0 +1,19 @@
+import { Filter } from './filter.model';
+
+export class FilterGroup {
+
+ id: string;
+ key: string;
+
+ constructor(id: string, public filters: Filter[]) {
+ this.id = id;
+ this.key = 'admin.reports.commons.filters.' + id;
+ filters.forEach(filter => {
+ filter.key = this.key + '.' + filter.id;
+ if (filter.hasTooltip) {
+ filter.tooltipKey = filter.key + '.tooltip';
+ }
+ });
+ }
+
+}
diff --git a/src/app/admin/admin-reports/filters-section/filter.model.ts b/src/app/admin/admin-reports/filters-section/filter.model.ts
new file mode 100644
index 0000000000..63eeb114cd
--- /dev/null
+++ b/src/app/admin/admin-reports/filters-section/filter.model.ts
@@ -0,0 +1,8 @@
+export class Filter {
+
+ key: string;
+ tooltipKey: string;
+
+ constructor(public id: string, public hasTooltip: boolean = false) {}
+
+}
diff --git a/src/app/admin/admin-reports/filters-section/filters-section.component.html b/src/app/admin/admin-reports/filters-section/filters-section.component.html
new file mode 100644
index 0000000000..1e7856f09c
--- /dev/null
+++ b/src/app/admin/admin-reports/filters-section/filters-section.component.html
@@ -0,0 +1,19 @@
+
+
diff --git a/src/app/suggestion-notifications/suggestion-targets/publication-claim/publication-claim.component.scss b/src/app/admin/admin-reports/filters-section/filters-section.component.scss
similarity index 100%
rename from src/app/suggestion-notifications/suggestion-targets/publication-claim/publication-claim.component.scss
rename to src/app/admin/admin-reports/filters-section/filters-section.component.scss
diff --git a/src/app/admin/admin-reports/filters-section/filters-section.component.spec.ts b/src/app/admin/admin-reports/filters-section/filters-section.component.spec.ts
new file mode 100644
index 0000000000..94f2753ec0
--- /dev/null
+++ b/src/app/admin/admin-reports/filters-section/filters-section.component.spec.ts
@@ -0,0 +1,101 @@
+import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { FiltersComponent } from './filters-section.component';
+import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
+import { TranslateLoaderMock } from 'src/app/shared/mocks/translate-loader.mock';
+import { FormBuilder } from '@angular/forms';
+
+describe('FiltersComponent', () => {
+ let component: FiltersComponent;
+ let fixture: ComponentFixture;
+ let formBuilder: FormBuilder;
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [FiltersComponent],
+ imports: [
+ TranslateModule.forRoot({
+ loader: {
+ provide: TranslateLoader,
+ useClass: TranslateLoaderMock
+ }
+ })
+ ],
+ providers: [
+ FormBuilder
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
+ });
+ }));
+
+ beforeEach(waitForAsync(() => {
+ formBuilder = TestBed.inject(FormBuilder);
+
+ fixture = TestBed.createComponent(FiltersComponent);
+ component = fixture.componentInstance;
+ component.filtersForm = FiltersComponent.formGroup(formBuilder);
+ fixture.detectChanges();
+ }));
+
+ const isOneSelected = (values: {}): boolean => {
+ let oneSelected = false;
+ let allFilters = FiltersComponent.FILTERS;
+ for (let i = 0; !oneSelected && i < allFilters.length; i++) {
+ let group = allFilters[i];
+ for (let j = 0; j < group.filters.length; j++) {
+ let filter = group.filters[j];
+ oneSelected = oneSelected || values[filter.id];
+ }
+ }
+ return oneSelected;
+ };
+
+ const isAllSelected = (values: {}): boolean => {
+ let allSelected = true;
+ let allFilters = FiltersComponent.FILTERS;
+ for (let i = 0; allSelected && i < allFilters.length; i++) {
+ let group = allFilters[i];
+ for (let j = 0; j < group.filters.length; j++) {
+ let filter = group.filters[j];
+ allSelected = allSelected && values[filter.id];
+ }
+ }
+ return allSelected;
+ };
+
+ it('should create the component', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should select all checkboxes', () => {
+ // By default, nothing is selected, so at least one item is not selected.
+ let values = component.filtersForm.value;
+ let allSelected: boolean = isAllSelected(values);
+ expect(allSelected).toBeFalse();
+
+ // Now we select everything...
+ component.selectAll();
+
+ // We must retrieve the form values again since selectAll() injects a new dictionary.
+ values = component.filtersForm.value;
+ allSelected = isAllSelected(values);
+ expect(allSelected).toBeTrue();
+ });
+
+ it('should deselect all checkboxes', () => {
+ // Since nothing is selected by default, we select at least an item
+ // so that deselectAll() actually deselects something.
+ let values = component.filtersForm.value;
+ values.is_item = true;
+ let oneSelected: boolean = isOneSelected(values);
+ expect(oneSelected).toBeTrue();
+
+ // Now we deselect everything...
+ component.deselectAll();
+
+ // We must retrieve the form values again since deselectAll() injects a new dictionary.
+ values = component.filtersForm.value;
+ oneSelected = isOneSelected(values);
+ expect(oneSelected).toBeFalse();
+ });
+});
diff --git a/src/app/admin/admin-reports/filters-section/filters-section.component.ts b/src/app/admin/admin-reports/filters-section/filters-section.component.ts
new file mode 100644
index 0000000000..d5dc074f08
--- /dev/null
+++ b/src/app/admin/admin-reports/filters-section/filters-section.component.ts
@@ -0,0 +1,149 @@
+import { Component, Input } from '@angular/core';
+import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
+import { FilterGroup } from './filter-group.model';
+import { Filter } from './filter.model';
+
+/**
+ * Component representing the Query Filters section used in both
+ * Filtered Collections and Filtered Items content reports
+ */
+@Component({
+ selector: 'ds-filters',
+ templateUrl: './filters-section.component.html',
+ styleUrls: ['./filters-section.component.scss']
+})
+export class FiltersComponent {
+
+ static FILTERS = [
+ new FilterGroup('property', [
+ new Filter('is_item'),
+ new Filter('is_withdrawn'),
+ new Filter('is_not_withdrawn'),
+ new Filter('is_discoverable'),
+ new Filter('is_not_discoverable')
+ ]),
+ new FilterGroup('bitstream', [
+ new Filter('has_multiple_originals'),
+ new Filter('has_no_originals'),
+ new Filter('has_one_original')
+ ]),
+ new FilterGroup('bitstream_mime', [
+ new Filter('has_doc_original'),
+ new Filter('has_image_original'),
+ new Filter('has_unsupp_type'),
+ new Filter('has_mixed_original'),
+ new Filter('has_pdf_original'),
+ new Filter('has_jpg_original'),
+ new Filter('has_small_pdf'),
+ new Filter('has_large_pdf'),
+ new Filter('has_doc_without_text')
+ ]),
+ new FilterGroup('mime', [
+ new Filter('has_only_supp_image_type'),
+ new Filter('has_unsupp_image_type'),
+ new Filter('has_only_supp_doc_type'),
+ new Filter('has_unsupp_doc_type')
+ ]),
+ new FilterGroup('bundle', [
+ new Filter('has_unsupported_bundle'),
+ new Filter('has_small_thumbnail'),
+ new Filter('has_original_without_thumbnail'),
+ new Filter('has_invalid_thumbnail_name'),
+ new Filter('has_non_generated_thumb'),
+ new Filter('no_license'),
+ new Filter('has_license_documentation')
+ ]),
+ new FilterGroup('permission', [
+ new Filter('has_restricted_original', true),
+ new Filter('has_restricted_thumbnail', true),
+ new Filter('has_restricted_metadata', true)
+ ])
+ ];
+
+ @Input() filtersForm: FormGroup;
+
+ static formGroup(formBuilder: FormBuilder): FormGroup {
+ let fields = {};
+ let allFilters = FiltersComponent.FILTERS;
+ for (let i = 0; i < allFilters.length; i++) {
+ let group = allFilters[i];
+ for (let j = 0; j < group.filters.length; j++) {
+ let filter = group.filters[j];
+ fields[filter.id] = new FormControl(false);
+ }
+ }
+ return formBuilder.group(fields);
+ }
+
+ static getFilter(filterId: string): Filter {
+ let allFilters = FiltersComponent.FILTERS;
+ for (let i = 0; i < allFilters.length; i++) {
+ let group = allFilters[i];
+ for (let j = 0; j < group.filters.length; j++) {
+ let filter = group.filters[j];
+ if (filter.id === filterId) {
+ return filter;
+ }
+ }
+ }
+ return undefined;
+ }
+
+ static getGroup(filterId: string): FilterGroup {
+ let allFilters = FiltersComponent.FILTERS;
+ for (let i = 0; i < allFilters.length; i++) {
+ let group = allFilters[i];
+ for (let j = 0; j < group.filters.length; j++) {
+ let filter = group.filters[j];
+ if (filter.id === filterId) {
+ return group;
+ }
+ }
+ }
+ return undefined;
+ }
+
+ static toQueryString(filters: Object): string {
+ let params = '';
+ let first = true;
+ for (const key in filters) {
+ if (filters[key]) {
+ if (first) {
+ first = false;
+ } else {
+ params += '&';
+ }
+ params += `filters=${key}`;
+ }
+ }
+ return params;
+ }
+
+ allFilters(): FilterGroup[] {
+ return FiltersComponent.FILTERS;
+ }
+
+ private setAllFilters(value: boolean) {
+ // I don't know why, but patchValue() with individual controls doesn't work.
+ // I therefore use setValue() with the whole set, which mercifully works...
+ let fields = {};
+ let allFilters = FiltersComponent.FILTERS;
+ for (let i = 0; i < allFilters.length; i++) {
+ let group = allFilters[i];
+ for (let j = 0; j < group.filters.length; j++) {
+ let filter = group.filters[j];
+ fields[filter.id] = value;
+ }
+ }
+ this.filtersForm.setValue(fields);
+ }
+
+ selectAll(): void {
+ this.setAllFilters(true);
+ }
+
+ deselectAll(): void {
+ this.setAllFilters(false);
+ }
+
+}
diff --git a/src/app/admin/admin-routing-paths.ts b/src/app/admin/admin-routing-paths.ts
index 30f801cecb..9c4c6eb15a 100644
--- a/src/app/admin/admin-routing-paths.ts
+++ b/src/app/admin/admin-routing-paths.ts
@@ -1,13 +1,30 @@
import { URLCombiner } from '../core/url-combiner/url-combiner';
import { getAdminModuleRoute } from '../app-routing-paths';
+import { getQualityAssuranceEditRoute } from './admin-notifications/admin-notifications-routing-paths';
export const REGISTRIES_MODULE_PATH = 'registries';
export const NOTIFICATIONS_MODULE_PATH = 'notifications';
+export const LDN_PATH = 'ldn';
+export const REPORTS_MODULE_PATH = 'reports';
+
+
export function getRegistriesModuleRoute() {
return new URLCombiner(getAdminModuleRoute(), REGISTRIES_MODULE_PATH).toString();
}
+export function getLdnServicesModuleRoute() {
+ return new URLCombiner(getAdminModuleRoute(), LDN_PATH).toString();
+}
+
export function getNotificationsModuleRoute() {
return new URLCombiner(getAdminModuleRoute(), NOTIFICATIONS_MODULE_PATH).toString();
}
+
+export function getNotificatioQualityAssuranceRoute() {
+ return new URLCombiner(`/${NOTIFICATIONS_MODULE_PATH}`, getQualityAssuranceEditRoute()).toString();
+}
+
+export function getReportsModuleRoute() {
+ return new URLCombiner(getAdminModuleRoute(), REPORTS_MODULE_PATH).toString();
+}
diff --git a/src/app/admin/admin-routing.module.ts b/src/app/admin/admin-routing.module.ts
index a7d19a6935..ae7d49a915 100644
--- a/src/app/admin/admin-routing.module.ts
+++ b/src/app/admin/admin-routing.module.ts
@@ -6,8 +6,15 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
-import { REGISTRIES_MODULE_PATH, NOTIFICATIONS_MODULE_PATH } from './admin-routing-paths';
+import {
+ LDN_PATH,
+ NOTIFICATIONS_MODULE_PATH,
+ REGISTRIES_MODULE_PATH, REPORTS_MODULE_PATH,
+} from './admin-routing-paths';
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
+import {
+ SiteAdministratorGuard
+} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
@NgModule({
imports: [
@@ -21,42 +28,65 @@ import { BatchImportPageComponent } from './admin-import-batch-page/batch-import
path: REGISTRIES_MODULE_PATH,
loadChildren: () => import('./admin-registries/admin-registries.module')
.then((m) => m.AdminRegistriesModule),
+ canActivate: [SiteAdministratorGuard]
},
{
path: 'search',
resolve: { breadcrumb: I18nBreadcrumbResolver },
component: AdminSearchPageComponent,
- data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }
+ data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' },
+ canActivate: [SiteAdministratorGuard]
},
{
path: 'workflow',
resolve: { breadcrumb: I18nBreadcrumbResolver },
component: AdminWorkflowPageComponent,
- data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' }
+ data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' },
+ canActivate: [SiteAdministratorGuard]
},
{
path: 'curation-tasks',
resolve: { breadcrumb: I18nBreadcrumbResolver },
component: AdminCurationTasksComponent,
- data: { title: 'admin.curation-tasks.title', breadcrumbKey: 'admin.curation-tasks' }
+ data: { title: 'admin.curation-tasks.title', breadcrumbKey: 'admin.curation-tasks' },
+ canActivate: [SiteAdministratorGuard]
},
{
path: 'metadata-import',
resolve: { breadcrumb: I18nBreadcrumbResolver },
component: MetadataImportPageComponent,
- data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }
+ data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' },
+ canActivate: [SiteAdministratorGuard]
},
{
path: 'batch-import',
resolve: { breadcrumb: I18nBreadcrumbResolver },
component: BatchImportPageComponent,
- data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }
+ data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' },
+ canActivate: [SiteAdministratorGuard]
},
{
path: 'system-wide-alert',
resolve: { breadcrumb: I18nBreadcrumbResolver },
loadChildren: () => import('../system-wide-alert/system-wide-alert.module').then((m) => m.SystemWideAlertModule),
- data: {title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert'}
+ data: {title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert'},
+ canActivate: [SiteAdministratorGuard]
+ },
+ {
+ path: LDN_PATH,
+ children: [
+ { path: '', pathMatch: 'full', redirectTo: 'services' },
+ {
+ path: 'services',
+ loadChildren: () => import('./admin-ldn-services/admin-ldn-services.module')
+ .then((m) => m.AdminLdnServicesModule),
+ }
+ ],
+ },
+ {
+ path: REPORTS_MODULE_PATH,
+ loadChildren: () => import('./admin-reports/admin-reports.module')
+ .then((m) => m.AdminReportsModule),
},
])
],
diff --git a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html
index 7f1e8716ba..24ba17fff4 100644
--- a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html
+++ b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html
@@ -1,23 +1,21 @@
-