diff --git a/.eslintrc.json b/.eslintrc.json index 985126bbc2..50a9be3d59 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,7 +8,10 @@ "eslint-plugin-deprecation", "unused-imports", "eslint-plugin-lodash", - "eslint-plugin-jsonc" + "eslint-plugin-jsonc", + "eslint-plugin-rxjs", + "eslint-plugin-simple-import-sort", + "eslint-plugin-import-newlines" ], "overrides": [ { @@ -27,17 +30,29 @@ "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking", "plugin:@angular-eslint/recommended", - "plugin:@angular-eslint/template/process-inline-templates" + "plugin:@angular-eslint/template/process-inline-templates", + "plugin:rxjs/recommended" ], "rules": { + "indent": [ + "error", + 2, + { + "SwitchCase": 1 + } + ], "max-classes-per-file": [ "error", 1 ], "comma-dangle": [ - "off", + "error", "always-multiline" ], + "object-curly-spacing": [ + "error", + "always" + ], "eol-last": [ "error", "always" @@ -104,15 +119,13 @@ "allowTernary": true } ], - "prefer-const": "off", // todo: re-enable & fix errors (more strict than it used to be in TSLint) + "prefer-const": "error", + "no-case-declarations": "error", + "no-extra-boolean-cast": "error", "prefer-spread": "off", "no-underscore-dangle": "off", - - // todo: disabled rules from eslint:recommended, consider re-enabling & fixing "no-prototype-builtins": "off", "no-useless-escape": "off", - "no-case-declarations": "off", - "no-extra-boolean-cast": "off", "@angular-eslint/directive-selector": [ "error", @@ -182,7 +195,7 @@ ], "@typescript-eslint/type-annotation-spacing": "error", "@typescript-eslint/unified-signatures": "error", - "@typescript-eslint/ban-types": "warn", // todo: deal with {} type issues & re-enable + "@typescript-eslint/ban-types": "error", "@typescript-eslint/no-floating-promises": "warn", "@typescript-eslint/no-misused-promises": "warn", "@typescript-eslint/restrict-plus-operands": "warn", @@ -202,14 +215,45 @@ "deprecation/deprecation": "warn", + "simple-import-sort/imports": "error", + "simple-import-sort/exports": "error", "import/order": "off", + "import/first": "error", + "import/newline-after-import": "error", + "import/no-duplicates": "error", "import/no-deprecated": "warn", "import/no-namespace": "error", + "import-newlines/enforce": [ + "error", + { + "items": 1, + "semi": true, + "forceSingleLine": true + } + ], + "unused-imports/no-unused-imports": "error", "lodash/import-scope": [ "error", "method" - ] + ], + + "rxjs/no-nested-subscribe": "off" // todo: go over _all_ cases + } + }, + { + "files": [ + "*.spec.ts" + ], + "parserOptions": { + "project": [ + "./tsconfig.json", + "./cypress/tsconfig.json" + ], + "createDefaultProgram": true + }, + "rules": { + "prefer-const": "off" } }, { @@ -218,12 +262,7 @@ ], "extends": [ "plugin:@angular-eslint/template/recommended" - ], - "rules": { - // todo: re-enable & fix errors - "@angular-eslint/template/no-negated-async": "off", - "@angular-eslint/template/eqeqeq": "off" - } + ] }, { "files": [ diff --git a/config/config.example.yml b/config/config.example.yml index 8b010ba6ea..36d6a009d3 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -131,6 +131,10 @@ submission: # NOTE: after how many time (milliseconds) submission is saved automatically # eg. timer: 5 * (1000 * 60); // 5 minutes timer: 0 + # Always show the duplicate detection section if enabled, even if there are no potential duplicates detected + # (a message will be displayed to indicate no matches were found) + duplicateDetection: + alwaysShowSection: false icons: metadata: # NOTE: example of configuration @@ -427,9 +431,67 @@ comcolSelectionSort: # Search settings -search: +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' ] + + +# Notify metrics +# Configuration for Notify Admin Dashboard for metrics visualization +notifyMetrics: + # Configuration for received messages +- title: 'admin-notify-dashboard.received-ldn' + boxes: + - color: '#B8DAFF' + title: 'admin-notify-dashboard.NOTIFY.incoming.accepted' + config: 'NOTIFY.incoming.accepted' + description: 'admin-notify-dashboard.NOTIFY.incoming.accepted.description' + - color: '#D4EDDA' + title: 'admin-notify-dashboard.NOTIFY.incoming.processed' + config: 'NOTIFY.incoming.processed' + description: 'admin-notify-dashboard.NOTIFY.incoming.processed.description' + - color: '#FDBBC7' + title: 'admin-notify-dashboard.NOTIFY.incoming.failure' + config: 'NOTIFY.incoming.failure' + description: 'admin-notify-dashboard.NOTIFY.incoming.failure.description' + - color: '#FDBBC7' + title: 'admin-notify-dashboard.NOTIFY.incoming.untrusted' + config: 'NOTIFY.incoming.untrusted' + description: 'admin-notify-dashboard.NOTIFY.incoming.untrusted.description' + - color: '#43515F' + title: 'admin-notify-dashboard.NOTIFY.incoming.involvedItems' + textColor: '#fff' + config: 'NOTIFY.incoming.involvedItems' + description: 'admin-notify-dashboard.NOTIFY.incoming.involvedItems.description' +# Configuration for outgoing messages +- title: 'admin-notify-dashboard.generated-ldn' + boxes: + - color: '#B8DAFF' + title: 'admin-notify-dashboard.NOTIFY.outgoing.queued' + config: 'NOTIFY.outgoing.queued' + description: 'admin-notify-dashboard.NOTIFY.outgoing.queued.description' + - color: '#FDEEBB' + title: 'admin-notify-dashboard.NOTIFY.outgoing.queued_for_retry' + config: 'NOTIFY.outgoing.queued_for_retry' + description: 'admin-notify-dashboard.NOTIFY.outgoing.queued_for_retry.description' + - color: '#FDBBC7' + title: 'admin-notify-dashboard.NOTIFY.outgoing.failure' + config: 'NOTIFY.outgoing.failure' + description: 'admin-notify-dashboard.NOTIFY.outgoing.failure.description' + - color: '#43515F' + title: 'admin-notify-dashboard.NOTIFY.outgoing.involvedItems' + textColor: '#fff' + config: 'NOTIFY.outgoing.involvedItems' + description: 'admin-notify-dashboard.NOTIFY.outgoing.involvedItems.description' + - color: '#D4EDDA' + title: 'admin-notify-dashboard.NOTIFY.outgoing.delivered' + config: 'NOTIFY.outgoing.delivered' + description: 'admin-notify-dashboard.NOTIFY.outgoing.delivered.description' + + + + + diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index f6c6865052..b2255a7da6 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -21,7 +21,6 @@ import './commands'; import 'cypress-axe'; import { DSPACE_XSRF_COOKIE } from 'src/app/core/xsrf/xsrf.constants'; - // Runs once before all tests before(() => { // Cypress doesn't have access to the running application in Node.js. diff --git a/karma.conf.js b/karma.conf.js index 8418312b1a..f96558bfaf 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -15,7 +15,10 @@ module.exports = function (config) { ], client: { clearContext: false, // leave Jasmine Spec Runner output visible in browser - captureConsole: false + captureConsole: false, + jasmine: { + failSpecWithNoExpectations: true + } }, coverageIstanbulReporter: { dir: require('path').join(__dirname, './coverage/dspace-angular'), diff --git a/package.json b/package.json index abd25f4148..c0a3843605 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "react-copy-to-clipboard": "^5.1.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", - "sanitize-html": "^2.10.0", + "sanitize-html": "^2.12.1", "sortablejs": "1.15.0", "uuid": "^8.3.2", "webfontloader": "1.6.28", @@ -170,9 +170,12 @@ "eslint": "^8.39.0", "eslint-plugin-deprecation": "^1.4.1", "eslint-plugin-import": "^2.27.5", + "eslint-plugin-import-newlines": "^1.3.1", "eslint-plugin-jsdoc": "^45.0.0", "eslint-plugin-jsonc": "^2.6.0", "eslint-plugin-lodash": "^7.4.0", + "eslint-plugin-rxjs": "^5.0.3", + "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-unused-imports": "^2.0.0", "express-static-gzip": "^2.1.7", "jasmine-core": "^3.8.0", diff --git a/src/app/access-control/access-control-routing-paths.ts b/src/app/access-control/access-control-routing-paths.ts index 31f39f1c47..06ae032194 100644 --- a/src/app/access-control/access-control-routing-paths.ts +++ b/src/app/access-control/access-control-routing-paths.ts @@ -1,5 +1,5 @@ -import { URLCombiner } from '../core/url-combiner/url-combiner'; import { getAccessControlModuleRoute } from '../app-routing-paths'; +import { URLCombiner } from '../core/url-combiner/url-combiner'; export const EPERSON_PATH = 'epeople'; diff --git a/src/app/access-control/access-control.module.ts b/src/app/access-control/access-control.module.ts index 3c18219571..6cc8c63165 100644 --- a/src/app/access-control/access-control.module.ts +++ b/src/app/access-control/access-control.module.ts @@ -1,5 +1,6 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { AbstractControl } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component'; @@ -47,9 +48,9 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher = providers: [ { provide: DYNAMIC_ERROR_MESSAGES_MATCHER, - useValue: ValidateEmailErrorStateMatcher + useValue: ValidateEmailErrorStateMatcher, }, - ] + ], }) /** * This module handles all components related to the access control pages diff --git a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.spec.ts b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.spec.ts index ffa87b2508..5c1a078093 100644 --- a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.spec.ts +++ b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.spec.ts @@ -1,16 +1,22 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; - -import { of } from 'rxjs'; -import { NgbAccordionModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + NgbAccordionModule, + NgbNavModule, +} from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; -import { BulkAccessBrowseComponent } from './bulk-access-browse.component'; +import { buildPaginatedList } from '../../../core/data/paginated-list.model'; +import { PageInfo } from '../../../core/shared/page-info.model'; import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service'; import { SelectableObject } from '../../../shared/object-list/selectable-list/selectable-list.service.spec'; -import { PageInfo } from '../../../core/shared/page-info.model'; -import { buildPaginatedList } from '../../../core/data/paginated-list.model'; import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { BulkAccessBrowseComponent } from './bulk-access-browse.component'; import { ThemeService } from '../../../shared/theme-support/theme.service'; import { getMockThemeService } from '../../../shared/mocks/theme-service.mock'; import { PaginationComponent } from '../../../shared/pagination/pagination.component'; @@ -49,8 +55,8 @@ describe('BulkAccessBrowseComponent', () => { { provide: ThemeService, useValue: getMockThemeService() }, ], schemas: [ - NO_ERRORS_SCHEMA - ] + NO_ERRORS_SCHEMA, + ], }) .overrideComponent(BulkAccessBrowseComponent, { remove: { @@ -96,7 +102,7 @@ describe('BulkAccessBrowseComponent', () => { 'elementsPerPage': 5, 'totalElements': 2, 'totalPages': 1, - 'currentPage': 1 + 'currentPage': 1, }), [selected1, selected2]); const rd = createSuccessfulRemoteDataObject(list); diff --git a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.ts b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.ts index 5c3da4bc08..9e96d997b7 100644 --- a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.ts +++ b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.ts @@ -1,4 +1,17 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; +import { + BehaviorSubject, + Subscription, +} from 'rxjs'; +import { + distinctUntilChanged, + map, +} from 'rxjs/operators'; import { BehaviorSubject, Subscription } from 'rxjs'; import { distinctUntilChanged, map } from 'rxjs/operators'; @@ -6,12 +19,14 @@ import { distinctUntilChanged, map } from 'rxjs/operators'; import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service'; import { SelectableListState } from '../../../shared/object-list/selectable-list/selectable-list.reducer'; +import { + buildPaginatedList, + PaginatedList, +} from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; -import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; -import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; -import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; import { PageInfo } from '../../../core/shared/page-info.model'; -import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; +import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; import { hasValue } from '../../../shared/empty.util'; import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { AsyncPipe, NgForOf, NgIf } from '@angular/common'; @@ -27,6 +42,11 @@ import { ListableObjectComponentLoaderComponent } from '../../../shared/object-collection/shared/listable-object/listable-object-component-loader.component'; import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-configuration.service'; +import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; +import { SelectableListState } from '../../../shared/object-list/selectable-list/selectable-list.reducer'; +import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; @Component({ selector: 'ds-bulk-access-browse', @@ -77,7 +97,7 @@ export class BulkAccessBrowseComponent implements OnInit, OnDestroy { paginationOptions$: BehaviorSubject = new BehaviorSubject(Object.assign(new PaginationComponentOptions(), { id: 'bas', pageSize: 5, - currentPage: 1 + currentPage: 1, })); /** @@ -95,20 +115,20 @@ export class BulkAccessBrowseComponent implements OnInit, OnDestroy { this.subs.push( this.selectableListService.getSelectableList(this.listId).pipe( distinctUntilChanged(), - map((list: SelectableListState) => this.generatePaginatedListBySelectedElements(list)) - ).subscribe(this.objectsSelected$) + map((list: SelectableListState) => this.generatePaginatedListBySelectedElements(list)), + ).subscribe(this.objectsSelected$), ); } pageNext() { this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, { - currentPage: this.paginationOptions$.value.currentPage + 1 + currentPage: this.paginationOptions$.value.currentPage + 1, })); } pagePrev() { this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, { - currentPage: this.paginationOptions$.value.currentPage - 1 + currentPage: this.paginationOptions$.value.currentPage - 1, })); } @@ -127,12 +147,12 @@ export class BulkAccessBrowseComponent implements OnInit, OnDestroy { elementsPerPage: this.paginationOptions$.value.pageSize, totalElements: list?.selection.length, totalPages: this.calculatePageCount(this.paginationOptions$.value.pageSize, list?.selection.length), - currentPage: this.paginationOptions$.value.currentPage + currentPage: this.paginationOptions$.value.currentPage, }); if (pageInfo.currentPage > pageInfo.totalPages) { pageInfo.currentPage = pageInfo.totalPages; this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, { - currentPage: pageInfo.currentPage + currentPage: pageInfo.currentPage, })); } return createSuccessfulRemoteDataObject(buildPaginatedList(pageInfo, list?.selection || [])); diff --git a/src/app/access-control/bulk-access/bulk-access.component.spec.ts b/src/app/access-control/bulk-access/bulk-access.component.spec.ts index 389b57a69c..57901c6347 100644 --- a/src/app/access-control/bulk-access/bulk-access.component.spec.ts +++ b/src/app/access-control/bulk-access/bulk-access.component.spec.ts @@ -1,21 +1,23 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; - +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; import { of } from 'rxjs'; -import { BulkAccessComponent } from './bulk-access.component'; -import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service'; -import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; -import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { Process } from '../../process-page/processes/process.model'; -import { RouterTestingModule } from '@angular/router/testing'; +import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { ThemeService } from '../../shared/theme-support/theme.service'; import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component'; +import { BulkAccessComponent } from './bulk-access.component'; describe('BulkAccessComponent', () => { let component: BulkAccessComponent; @@ -34,35 +36,35 @@ describe('BulkAccessComponent', () => { 'startDate': { 'year': 2026, 'month': 5, - 'day': 31 + 'day': 31, }, - 'endDate': null - } + 'endDate': null, + }, ], 'state': { 'item': { 'toggleStatus': true, - 'accessMode': 'replace' + 'accessMode': 'replace', }, 'bitstream': { 'toggleStatus': false, 'accessMode': '', 'changesLimit': '', - 'selectedBitstreams': [] - } - } + 'selectedBitstreams': [], + }, + }, }; const mockFile = { 'uuids': [ - '1234', '5678' + '1234', '5678', ], - 'file': { } + 'file': { }, }; const mockSettings: any = jasmine.createSpyObj('AccessControlFormContainerComponent', { getValue: jasmine.createSpy('getValue'), - reset: jasmine.createSpy('reset') + reset: jasmine.createSpy('reset'), }); const selection: any[] = [{ indexableObject: { uuid: '1234' } }, { indexableObject: { uuid: '5678' } }]; const selectableListState: SelectableListState = { id: 'test', selection }; @@ -83,7 +85,7 @@ describe('BulkAccessComponent', () => { { provide: SelectableListService, useValue: selectableListServiceMock }, { provide: ThemeService, useValue: getMockThemeService() } ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }) .overrideComponent(BulkAccessComponent, { remove: { diff --git a/src/app/access-control/bulk-access/bulk-access.component.ts b/src/app/access-control/bulk-access/bulk-access.component.ts index 85fde3b208..658aca0d78 100644 --- a/src/app/access-control/bulk-access/bulk-access.component.ts +++ b/src/app/access-control/bulk-access/bulk-access.component.ts @@ -1,14 +1,23 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; +import { + Component, + OnInit, + ViewChild, +} from '@angular/core'; +import { + BehaviorSubject, + Subscription, +} from 'rxjs'; +import { + distinctUntilChanged, + map, +} from 'rxjs/operators'; -import { BehaviorSubject, Subscription } from 'rxjs'; -import { distinctUntilChanged, map } from 'rxjs/operators'; - -import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component'; import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service'; import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer'; import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; import { TranslateModule } from '@ngx-translate/core'; import { BulkAccessBrowseComponent } from './browse/bulk-access-browse.component'; +import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component'; @Component({ selector: 'ds-bulk-access', @@ -45,7 +54,7 @@ export class BulkAccessComponent implements OnInit { constructor( private bulkAccessControlService: BulkAccessControlService, - private selectableListService: SelectableListService + private selectableListService: SelectableListService, ) { } @@ -53,8 +62,8 @@ export class BulkAccessComponent implements OnInit { this.subs.push( this.selectableListService.getSelectableList(this.listId).pipe( distinctUntilChanged(), - map((list: SelectableListState) => this.generateIdListBySelectedElements(list)) - ).subscribe(this.objectsSelected$) + map((list: SelectableListState) => this.generateIdListBySelectedElements(list)), + ).subscribe(this.objectsSelected$), ); } @@ -82,12 +91,12 @@ export class BulkAccessComponent implements OnInit { const { file } = this.bulkAccessControlService.createPayloadFile({ bitstreamAccess, itemAccess, - state: settings.state + state: settings.state, }); this.bulkAccessControlService.executeScript( this.objectsSelected$.value || [], - file + file, ).subscribe(); } diff --git a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.spec.ts b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.spec.ts index bed7084d83..bcc42e324f 100644 --- a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.spec.ts +++ b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.spec.ts @@ -1,6 +1,11 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; + import { BulkAccessSettingsComponent } from './bulk-access-settings.component'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { @@ -18,28 +23,28 @@ describe('BulkAccessSettingsComponent', () => { 'startDate': { 'year': 2026, 'month': 5, - 'day': 31 + 'day': 31, }, - 'endDate': null - } + 'endDate': null, + }, ], 'state': { 'item': { 'toggleStatus': true, - 'accessMode': 'replace' + 'accessMode': 'replace', }, 'bitstream': { 'toggleStatus': false, 'accessMode': '', 'changesLimit': '', - 'selectedBitstreams': [] - } - } + 'selectedBitstreams': [], + }, + }, }; const mockControl: any = jasmine.createSpyObj('AccessControlFormContainerComponent', { getFormValue: jasmine.createSpy('getFormValue'), - reset: jasmine.createSpy('reset') + reset: jasmine.createSpy('reset'), }); beforeEach(async () => { diff --git a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts index a19adecb34..b7d5caa9c3 100644 --- a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts +++ b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts @@ -1,7 +1,9 @@ -import { Component, ViewChild } from '@angular/core'; import { - AccessControlFormContainerComponent -} from '../../../shared/access-control-form-container/access-control-form-container.component'; + Component, + ViewChild, +} from '@angular/core'; + +import { AccessControlFormContainerComponent } from '../../../shared/access-control-form-container/access-control-form-container.component'; import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; import { NgIf } from '@angular/common'; diff --git a/src/app/access-control/epeople-registry/epeople-registry.actions.ts b/src/app/access-control/epeople-registry/epeople-registry.actions.ts index a07ea37df2..e6e7608ba3 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.actions.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.actions.ts @@ -1,5 +1,6 @@ /* eslint-disable max-classes-per-file */ import { Action } from '@ngrx/store'; + import { EPerson } from '../../core/eperson/models/eperson.model'; import { type } from '../../shared/ngrx/type'; diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.html b/src/app/access-control/epeople-registry/epeople-registry.component.html index bf7b9a2060..92968d2e28 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.html +++ b/src/app/access-control/epeople-registry/epeople-registry.component.html @@ -43,7 +43,7 @@ - diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts b/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts index 1cdec13271..19d342f31c 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts @@ -1,31 +1,61 @@ -import { Router } from '@angular/router'; -import { Observable, of as observableOf } from 'rxjs'; import { CommonModule } from '@angular/common'; -import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { BrowserModule, By } from '@angular/platform-browser'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; +import { + DebugElement, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { + BrowserModule, + By, +} from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { + NgbModal, + NgbModule, +} from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { FindListOptions } from '../../core/data/find-list-options.model'; +import { + buildPaginatedList, + PaginatedList, +} from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; +import { RequestService } from '../../core/data/request.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { EPerson } from '../../core/eperson/models/eperson.model'; +import { PaginationService } from '../../core/pagination/pagination.service'; import { PageInfo } from '../../core/shared/page-info.model'; import { FormBuilderService } from '../../shared/form/builder/form-builder.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { EPeopleRegistryComponent } from './epeople-registry.component'; -import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock'; -import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; -import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { + EPersonMock, + EPersonMock2, +} from '../../shared/testing/eperson.mock'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { RequestService } from '../../core/data/request.service'; import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; -import { FindListOptions } from '../../core/data/find-list-options.model'; +import { RouterStub } from '../../shared/testing/router.stub'; +import { EPeopleRegistryComponent } from './epeople-registry.component'; import { EPersonFormComponent } from './eperson-form/eperson-form.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { PaginationComponent } from '../../shared/pagination/pagination.component'; @@ -35,17 +65,15 @@ import { RouterMock } from '../../shared/mocks/router.mock'; describe('EPeopleRegistryComponent', () => { let component: EPeopleRegistryComponent; let fixture: ComponentFixture; - let translateService: TranslateService; let builderService: FormBuilderService; - let mockEPeople; + let mockEPeople: EPerson[]; let ePersonDataServiceStub: any; let authorizationService: AuthorizationDataService; - let modalService; + let modalService: NgbModal; + let paginationService: PaginationServiceStub; - let paginationService; - - beforeEach(waitForAsync(() => { + beforeEach(waitForAsync(async () => { jasmine.getEnv().allowRespy(true); mockEPeople = [EPersonMock, EPersonMock2]; ePersonDataServiceStub = { @@ -56,7 +84,7 @@ describe('EPeopleRegistryComponent', () => { elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, - currentPage: 1 + currentPage: 1, }), this.allEpeople)); }, getActiveEPerson(): Observable { @@ -71,7 +99,7 @@ describe('EPeopleRegistryComponent', () => { elementsPerPage: [result].length, totalElements: [result].length, totalPages: 1, - currentPage: 1 + currentPage: 1, }), [result])); } if (scope === 'metadata') { @@ -80,7 +108,7 @@ describe('EPeopleRegistryComponent', () => { elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, - currentPage: 1 + currentPage: 1, }), this.allEpeople)); } const result = this.allEpeople.find((ePerson: EPerson) => { @@ -90,20 +118,20 @@ describe('EPeopleRegistryComponent', () => { elementsPerPage: [result].length, totalElements: [result].length, totalPages: 1, - currentPage: 1 + currentPage: 1, }), [result])); } return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({ elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, - currentPage: 1 + currentPage: 1, }), this.allEpeople)); }, deleteEPerson(ePerson: EPerson): Observable { this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => { return (ePerson2.uuid !== ePerson.uuid); - }); + }); return observableOf(true); }, editEPerson(ePerson: EPerson) { @@ -117,23 +145,17 @@ describe('EPeopleRegistryComponent', () => { }, getEPeoplePageRouterLink(): string { return '/access-control/epeople'; - } + }, }; authorizationService = jasmine.createSpyObj('authorizationService', { - isAuthorized: observableOf(true) + isAuthorized: observableOf(true), }); builderService = getMockFormBuilderService(); - translateService = getMockTranslateService(); paginationService = new PaginationServiceStub(); - TestBed.configureTestingModule({ + awaitTestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, RouterTestingModule.withRoutes([]), - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }), EPeopleRegistryComponent], + TranslateModule.forRoot(), EPeopleRegistryComponent], providers: [ {provide: EPersonDataService, useValue: ePersonDataServiceStub}, {provide: NotificationsService, useValue: new NotificationsServiceStub()}, @@ -141,9 +163,9 @@ describe('EPeopleRegistryComponent', () => { {provide: FormBuilderService, useValue: builderService}, {provide: Router, useValue: new RouterMock()}, {provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])}, - {provide: PaginationService, useValue: paginationService} + { provide: PaginationService, useValue: paginationService }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }) .overrideComponent(EPeopleRegistryComponent, { remove: { @@ -160,7 +182,7 @@ describe('EPeopleRegistryComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(EPeopleRegistryComponent); component = fixture.componentInstance; - modalService = (component as any).modalService; + modalService = TestBed.inject(NgbModal); spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) })); fixture.detectChanges(); }); @@ -170,10 +192,10 @@ describe('EPeopleRegistryComponent', () => { }); it('should display list of ePeople', () => { - const ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child')); + const ePeopleIdsFound: DebugElement[] = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child')); expect(ePeopleIdsFound.length).toEqual(2); mockEPeople.map((ePerson: EPerson) => { - expect(ePeopleIdsFound.find((foundEl) => { + expect(ePeopleIdsFound.find((foundEl: DebugElement) => { return (foundEl.nativeElement.textContent.trim() === ePerson.uuid); })).toBeTruthy(); }); @@ -181,7 +203,7 @@ describe('EPeopleRegistryComponent', () => { describe('search', () => { describe('when searching with scope/query (scope metadata)', () => { - let ePeopleIdsFound; + let ePeopleIdsFound: DebugElement[]; beforeEach(fakeAsync(() => { component.search({ scope: 'metadata', query: EPersonMock2.name }); tick(); @@ -191,14 +213,14 @@ describe('EPeopleRegistryComponent', () => { it('should display search result', () => { expect(ePeopleIdsFound.length).toEqual(1); - expect(ePeopleIdsFound.find((foundEl) => { + expect(ePeopleIdsFound.find((foundEl: DebugElement) => { return (foundEl.nativeElement.textContent.trim() === EPersonMock2.uuid); })).toBeTruthy(); }); }); describe('when searching with scope/query (scope email)', () => { - let ePeopleIdsFound; + let ePeopleIdsFound: DebugElement[]; beforeEach(fakeAsync(() => { component.search({ scope: 'email', query: EPersonMock.email }); tick(); @@ -208,7 +230,7 @@ describe('EPeopleRegistryComponent', () => { it('should display search result', () => { expect(ePeopleIdsFound.length).toEqual(1); - expect(ePeopleIdsFound.find((foundEl) => { + expect(ePeopleIdsFound.find((foundEl: DebugElement) => { return (foundEl.nativeElement.textContent.trim() === EPersonMock.uuid); })).toBeTruthy(); }); @@ -224,7 +246,7 @@ describe('EPeopleRegistryComponent', () => { const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton')); deleteButtons[0].triggerEventHandler('click', { preventDefault: () => {/**/ - } + }, }); tick(); fixture.detectChanges(); @@ -240,19 +262,12 @@ describe('EPeopleRegistryComponent', () => { }); }); - describe('delete EPerson button when the isAuthorized returns false', () => { - let ePeopleDeleteButton; - beforeEach(() => { - spyOn(authorizationService, 'isAuthorized').and.returnValue(observableOf(false)); - component.initialisePage(); - fixture.detectChanges(); - }); - it('should be disabled', () => { - ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button')); - ePeopleDeleteButton.forEach((deleteButton: DebugElement) => { - expect(deleteButton.nativeElement.disabled).toBe(true); - }); - }); + it('should hide delete EPerson button when the isAuthorized returns false', () => { + spyOn(authorizationService, 'isAuthorized').and.returnValue(observableOf(false)); + component.initialisePage(); + fixture.detectChanges(); + + expect(fixture.debugElement.query(By.css('#epeople tr td div button.delete-button'))).toBeNull(); }); }); diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.ts b/src/app/access-control/epeople-registry/epeople-registry.component.ts index 26fcb98064..c149db0457 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.component.ts @@ -1,13 +1,44 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { + Component, + OnDestroy, + OnInit, +} from '@angular/core'; import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms'; import { Router, RouterModule } from '@angular/router'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs'; -import { map, switchMap, take } from 'rxjs/operators'; -import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; +import { + BehaviorSubject, + combineLatest, + Observable, + Subscription, +} from 'rxjs'; +import { + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { + buildPaginatedList, + PaginatedList, +} from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; +import { RequestService } from '../../core/data/request.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { EPerson } from '../../core/eperson/models/eperson.model'; +import { EpersonDtoModel } from '../../core/eperson/models/eperson-dto.model'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { NoContent } from '../../core/shared/NoContent.model'; +import { + getAllSucceededRemoteData, + getFirstCompletedRemoteData, +} from '../../core/shared/operators'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component'; import { hasValue } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; @@ -27,6 +58,10 @@ import { EPersonFormComponent } from './eperson-form/eperson-form.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { PaginationComponent } from '../../shared/pagination/pagination.component'; import { getEPersonEditRoute, getEPersonsRoute } from '../access-control-routing-paths'; +import { + getEPersonEditRoute, + getEPersonsRoute, +} from '../access-control-routing-paths'; @Component({ selector: 'ds-epeople-registry', @@ -79,7 +114,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { id: 'elp', pageSize: 5, - currentPage: 1 + currentPage: 1, }); // The search form @@ -127,7 +162,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { */ initialisePage() { this.searching$.next(true); - this.search({scope: this.currentSearchScope, query: this.currentSearchQuery}); + this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery }); this.subs.push(this.ePeople$.pipe( switchMap((epeople: PaginatedList) => { if (epeople.pageInfo.totalElements > 0) { @@ -138,7 +173,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { epersonDtoModel.ableToDelete = authorized; epersonDtoModel.eperson = eperson; return epersonDtoModel; - }) + }), ); })).pipe(map((dtos: EpersonDtoModel[]) => { return buildPaginatedList(epeople.pageInfo, dtos); @@ -164,34 +199,34 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { } this.findListOptionsSub = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe( switchMap((findListOptions) => { - const query: string = data.query; - const scope: string = data.scope; - if (query != null && this.currentSearchQuery !== query) { - void this.router.navigate([getEPersonsRoute()], { - queryParamsHandling: 'merge' - }); - this.currentSearchQuery = query; - this.paginationService.resetPage(this.config.id); - } - if (scope != null && this.currentSearchScope !== scope) { - void this.router.navigate([getEPersonsRoute()], { - queryParamsHandling: 'merge' - }); - this.currentSearchScope = scope; - this.paginationService.resetPage(this.config.id); - - } - return this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, { - currentPage: findListOptions.currentPage, - elementsPerPage: findListOptions.pageSize + const query: string = data.query; + const scope: string = data.scope; + if (query != null && this.currentSearchQuery !== query) { + void this.router.navigate([getEPersonsRoute()], { + queryParamsHandling: 'merge', }); + this.currentSearchQuery = query; + this.paginationService.resetPage(this.config.id); } + if (scope != null && this.currentSearchScope !== scope) { + void this.router.navigate([getEPersonsRoute()], { + queryParamsHandling: 'merge', + }); + this.currentSearchScope = scope; + this.paginationService.resetPage(this.config.id); + + } + return this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, { + currentPage: findListOptions.currentPage, + elementsPerPage: findListOptions.pageSize, + }); + }, ), getAllSucceededRemoteData(), ).subscribe((peopleRD) => { - this.ePeople$.next(peopleRD.payload); - this.pageInfoState$.next(peopleRD.payload.pageInfo); - } + this.ePeople$.next(peopleRD.payload); + this.pageInfoState$.next(peopleRD.payload.pageInfo); + }, ); } @@ -201,7 +236,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { */ isActive(eperson: EPerson): Observable { return this.getActiveEPerson().pipe( - map((activeEPerson) => eperson === activeEPerson) + map((activeEPerson) => eperson === activeEPerson), ); } @@ -230,7 +265,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { if (hasValue(ePerson.id)) { this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData) => { if (restResponse.hasSucceeded) { - this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: this.dsoNameService.getName(ePerson)})); + this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(ePerson) })); } else { this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { id: ePerson.id, statusCode: restResponse.statusCode, errorMessage: restResponse.errorMessage })); } @@ -261,7 +296,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { this.searchForm.patchValue({ query: '', }); - this.search({query: ''}); + this.search({ query: '' }); } getEditEPeoplePage(id: string): string { diff --git a/src/app/access-control/epeople-registry/epeople-registry.reducers.spec.ts b/src/app/access-control/epeople-registry/epeople-registry.reducers.spec.ts index 7158acc79b..6bee3f84e2 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.reducers.spec.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.reducers.spec.ts @@ -1,6 +1,12 @@ -import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from './epeople-registry.actions'; -import { ePeopleRegistryReducer, EPeopleRegistryState } from './epeople-registry.reducers'; import { EPersonMock } from '../../shared/testing/eperson.mock'; +import { + EPeopleRegistryCancelEPersonAction, + EPeopleRegistryEditEPersonAction, +} from './epeople-registry.actions'; +import { + ePeopleRegistryReducer, + EPeopleRegistryState, +} from './epeople-registry.reducers'; const initialState: EPeopleRegistryState = { editEPerson: null, diff --git a/src/app/access-control/epeople-registry/epeople-registry.reducers.ts b/src/app/access-control/epeople-registry/epeople-registry.reducers.ts index 1e0319f3ba..3bab6769e1 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.reducers.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.reducers.ts @@ -2,7 +2,7 @@ import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPeopleRegistryAction, EPeopleRegistryActionTypes, - EPeopleRegistryEditEPersonAction + EPeopleRegistryEditEPersonAction, } from './epeople-registry.actions'; /** @@ -30,13 +30,13 @@ export function ePeopleRegistryReducer(state = initialState, action: EPeopleRegi case EPeopleRegistryActionTypes.EDIT_EPERSON: { return Object.assign({}, state, { - editEPerson: (action as EPeopleRegistryEditEPersonAction).eperson + editEPerson: (action as EPeopleRegistryEditEPersonAction).eperson, }); } case EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON: { return Object.assign({}, state, { - editEPerson: null + editEPerson: null, }); } diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html index 747d30bb89..9168bbaf8e 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html @@ -25,7 +25,7 @@
-
@@ -47,13 +47,13 @@

{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}

- + @@ -68,7 +68,7 @@ - + {{group.id}} -