From a9d269c9d781bedda6c4ec23ec714dc06c215290 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Tue, 29 Apr 2025 18:04:06 +0200 Subject: [PATCH 01/22] [DURACOM-317] first part implementation Audit feature --- src/app/app-routes.ts | 5 + src/app/app.menus.ts | 2 + src/app/audit-page/audit-page-routes.ts | 38 +++ .../audit-page/audit-page.resolver.spec.ts | 37 +++ src/app/audit-page/audit-page.resolver.ts | 30 +++ .../detail/audit-detail.component.html | 33 +++ .../detail/audit-detail.component.ts | 83 +++++++ .../object-audit-overview.component.html | 67 ++++++ .../object-audit-overview.component.ts | 185 +++++++++++++++ .../overview/audit-overview.component.html | 47 ++++ .../overview/audit-overview.component.spec.ts | 153 ++++++++++++ .../overview/audit-overview.component.ts | 127 ++++++++++ src/app/core/audit/audit-data.service.spec.ts | 220 ++++++++++++++++++ src/app/core/audit/audit-data.service.ts | 159 +++++++++++++ src/app/core/audit/model/audit.model.ts | 121 ++++++++++ .../core/audit/model/audit.resource-type.ts | 9 + src/app/core/data-services-map.ts | 2 + src/app/core/data/collection-data.service.ts | 7 +- src/app/core/provide-core.ts | 2 + .../menu/providers/audit-item.menu.spec.ts | 45 ++++ .../shared/menu/providers/audit-item.menu.ts | 55 +++++ src/assets/i18n/en.json5 | 46 ++++ 22 files changed, 1471 insertions(+), 2 deletions(-) create mode 100644 src/app/audit-page/audit-page-routes.ts create mode 100644 src/app/audit-page/audit-page.resolver.spec.ts create mode 100644 src/app/audit-page/audit-page.resolver.ts create mode 100644 src/app/audit-page/detail/audit-detail.component.html create mode 100644 src/app/audit-page/detail/audit-detail.component.ts create mode 100644 src/app/audit-page/object-audit-overview/object-audit-overview.component.html create mode 100644 src/app/audit-page/object-audit-overview/object-audit-overview.component.ts create mode 100644 src/app/audit-page/overview/audit-overview.component.html create mode 100644 src/app/audit-page/overview/audit-overview.component.spec.ts create mode 100644 src/app/audit-page/overview/audit-overview.component.ts create mode 100644 src/app/core/audit/audit-data.service.spec.ts create mode 100644 src/app/core/audit/audit-data.service.ts create mode 100644 src/app/core/audit/model/audit.model.ts create mode 100644 src/app/core/audit/model/audit.resource-type.ts create mode 100644 src/app/shared/menu/providers/audit-item.menu.spec.ts create mode 100644 src/app/shared/menu/providers/audit-item.menu.ts diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index dc9b4276345..297ced23a1c 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -265,6 +265,11 @@ export const APP_ROUTES: Route[] = [ loadChildren: () => import('./access-control/access-control-routes').then((m) => m.ROUTES), canActivate: [groupAdministratorGuard, endUserAgreementCurrentUserGuard], }, + { + path: 'auditlogs', + loadChildren: () => import('./audit-page/audit-page-routes').then((m) => m.ROUTES), + canActivate: [authenticatedGuard, endUserAgreementCurrentUserGuard], + }, { path: 'subscriptions', loadChildren: () => import('./subscriptions-page/subscriptions-page-routes') diff --git a/src/app/app.menus.ts b/src/app/app.menus.ts index e63b948c2f7..112df24a8ca 100644 --- a/src/app/app.menus.ts +++ b/src/app/app.menus.ts @@ -10,6 +10,7 @@ import { MenuID } from './shared/menu/menu-id.model'; import { MenuRoute } from './shared/menu/menu-route.model'; import { AccessControlMenuProvider } from './shared/menu/providers/access-control.menu'; import { AdminSearchMenuProvider } from './shared/menu/providers/admin-search.menu'; +import { AuditLogsMenuProvider } from './shared/menu/providers/audit-item.menu'; import { BrowseMenuProvider } from './shared/menu/providers/browse.menu'; import { CoarNotifyMenuProvider } from './shared/menu/providers/coar-notify.menu'; import { SubscribeMenuProvider } from './shared/menu/providers/comcol-subscribe.menu'; @@ -72,6 +73,7 @@ export const MENUS = buildMenuStructure({ HealthMenuProvider, SystemWideAlertMenuProvider, CoarNotifyMenuProvider, + AuditLogsMenuProvider, ], [MenuID.DSO_EDIT]: [ DsoOptionMenuProvider.withSubs([ diff --git a/src/app/audit-page/audit-page-routes.ts b/src/app/audit-page/audit-page-routes.ts new file mode 100644 index 00000000000..71e2dcbffb5 --- /dev/null +++ b/src/app/audit-page/audit-page-routes.ts @@ -0,0 +1,38 @@ +import { Route } from '@angular/router'; + +import { authenticatedGuard } from '../core/auth/authenticated.guard'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { auditPageResolver } from './audit-page.resolver'; +import { AuditDetailComponent } from './detail/audit-detail.component'; +import { ObjectAuditOverviewComponent } from './object-audit-overview/object-audit-overview.component'; +import { AuditOverviewComponent } from './overview/audit-overview.component'; + +export const ROUTES: Route[] = [ + { + path: '', + resolve: { breadcrumb: i18nBreadcrumbResolver }, + data: { breadcrumbKey: 'audit.overview' }, + canActivate: [authenticatedGuard], + children: [ + { + path: '', + component: AuditOverviewComponent, + data: { title: 'audit.overview.title' }, + }, + { + path: ':id', + component: AuditDetailComponent, + resolve: { + process: auditPageResolver, + // TODO: breadcrumbs resolver + }, + }, + { + path: 'object/:objectId', + component: ObjectAuditOverviewComponent, + // TODO: breadcrumbs resolver + }, + ], + }, + +]; diff --git a/src/app/audit-page/audit-page.resolver.spec.ts b/src/app/audit-page/audit-page.resolver.spec.ts new file mode 100644 index 00000000000..a7b10dff3ec --- /dev/null +++ b/src/app/audit-page/audit-page.resolver.spec.ts @@ -0,0 +1,37 @@ +import { TestBed } from '@angular/core/testing'; +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; + +import { AuditDataService } from '../core/audit/audit-data.service'; +import { Audit } from '../core/audit/model/audit.model'; +import { RemoteData } from '../core/data/remote-data'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { auditPageResolver } from './audit-page.resolver'; + +describe('auditPageResolver', () => { + let auditService: any; + + beforeEach(() => { + auditService = { + findById: jasmine.createSpy('findById').and.callFake((id: string) => createSuccessfulRemoteDataObject$({ id })), + }; + + TestBed.configureTestingModule({ + providers: [ + { provide: AuditDataService, useValue: auditService }, + ], + }); + }); + + it('should resolve an audit with the correct id', (done) => { + const uuid = '1234-65487-12354-1235'; + const obs = TestBed.runInInjectionContext(() => { + return auditPageResolver({ params: { id: uuid } } as any, undefined); + }) as Observable>; + + obs.pipe(first()).subscribe((resolved) => { + expect(resolved.payload.id).toEqual(uuid); + done(); + }); + }); +}); diff --git a/src/app/audit-page/audit-page.resolver.ts b/src/app/audit-page/audit-page.resolver.ts new file mode 100644 index 00000000000..8c98361744c --- /dev/null +++ b/src/app/audit-page/audit-page.resolver.ts @@ -0,0 +1,30 @@ +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; + +import { AuditDataService } from '../core/audit/audit-data.service'; +import { Audit } from '../core/audit/model/audit.model'; +import { RemoteData } from '../core/data/remote-data'; +import { getFirstSucceededRemoteData } from '../core/shared/operators'; + + +/** + * Method for resolving an audit based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable<> Emits the found process based on the parameters in the current route, + * or an error if something went wrong + */ +export const auditPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +): Observable> => { + const auditService = inject(AuditDataService); + return auditService.findById(route.params.id).pipe( + getFirstSucceededRemoteData(), + ); +}; diff --git a/src/app/audit-page/detail/audit-detail.component.html b/src/app/audit-page/detail/audit-detail.component.html new file mode 100644 index 00000000000..c6c0b5f950d --- /dev/null +++ b/src/app/audit-page/detail/audit-detail.component.html @@ -0,0 +1,33 @@ +
+
+

{{'audit.detail.title' | translate }}

+
+ + @if (audit) { +

{{ 'audit.detail.id' | translate}}

+
{{ audit.id }}
+ +

{{ 'audit.detail.eventType' | translate}}

+
{{ audit.eventType }}
+ +

{{ 'audit.detail.subjectUUID' | translate}}

+
{{ audit.subjectUUID }}
+ +

{{ 'audit.detail.subjectType' | translate}}

+
{{ audit.subjectType }}
+ +

{{ 'audit.detail.eperson' | translate}}

+
{{ePersonName}}
+ +

{{ 'audit.detail.timeStamp' | translate}}

+
{{ audit.timeStamp | date:dateFormat}}
+ } + + + {{'audit.detail.back' | translate}} + + @if (audit.objectUUID) { + {{'audit.detail.back.subject' | translate}} + } + +
diff --git a/src/app/audit-page/detail/audit-detail.component.ts b/src/app/audit-page/detail/audit-detail.component.ts new file mode 100644 index 00000000000..29cbd8a3e96 --- /dev/null +++ b/src/app/audit-page/detail/audit-detail.component.ts @@ -0,0 +1,83 @@ +import { + AsyncPipe, + DatePipe, + NgIf, +} from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + OnInit, +} from '@angular/core'; +import { + ActivatedRoute, + Router, + RouterLink, +} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { AuditDataService } from '../../core/audit/audit-data.service'; +import { Audit } from '../../core/audit/model/audit.model'; +import { AuthService } from '../../core/auth/auth.service'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { RemoteData } from '../../core/data/remote-data'; +import { redirectOn4xx } from '../../core/shared/authorized.operators'; +import { VarDirective } from '../../shared/utils/var.directive'; + +/** + * A component displaying detailed information about a DSpace Audit + */ +@Component({ + selector: 'ds-audit-detail', + templateUrl: './audit-detail.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + NgIf, + AsyncPipe, + TranslateModule, + VarDirective, + DatePipe, + RouterLink, + ], + standalone: true, +}) +export class AuditDetailComponent implements OnInit { + + /** + * The Audit's Remote Data + */ + auditRD$: Observable>; + + /** + * Date format to use for start and end time of audits + */ + dateFormat = 'yyyy-MM-dd HH:mm:ss'; + + constructor(protected authService: AuthService, + protected route: ActivatedRoute, + protected router: Router, + protected auditService: AuditDataService, + protected nameService: DSONameService) { + } + + /** + * Initialize component properties + * Display a 404 if the audit doesn't exist + */ + ngOnInit(): void { + this.auditRD$ = this.route.data.pipe( + map((data) => data.process as RemoteData), + redirectOn4xx(this.router, this.authService), + ); + } + + /** + * Get the name of an EPerson by ID + * @param audit Audit object + */ + getEpersonName(audit: Audit): Observable { + return this.auditService.getEpersonName(audit); + } + +} diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html new file mode 100644 index 00000000000..84ed4707468 --- /dev/null +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html @@ -0,0 +1,67 @@ +
+
+

{{'audit.object.overview.title' | translate}}

+
+ + @if (object) { +

{{ object.name }} ({{object.type}})

+ @if ((auditsRD$ | async)?.payload; as audits) { + @if (audits.totalElements === 0) { +
+ No audits found. +
+ } + + @if (audits.totalElements > 0) { + + +
+ + + + + + + + + + + + + + + + + + + +
{{ 'audit.overview.table.entityType' | translate }}{{ 'audit.overview.table.eperson' | translate }}{{ 'audit.overview.table.timestamp' | translate }}Other Object
{{ audit.eventType }}{{ePersonName}}{{ audit.timeStamp | date:dateFormat}} + @if (object.id === audit.objectUUID) { + + + @if ((getOtherObject(audit, object.id) | async); as subject) { + @if (subject) { + {{ subject.name }} ({{ subject.type }}) + } + } + + } +
+
+
+ } + Back to Item + + + + } + @if ((auditsRD$ | async)?.statusCode === 404) { +

{{'audit.object.overview.disabled.message' | translate}}

+ } + } + +
diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts new file mode 100644 index 00000000000..6e7962a99f9 --- /dev/null +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts @@ -0,0 +1,185 @@ +import { + AsyncPipe, + DatePipe, + NgForOf, + NgIf, +} from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; +import { + ActivatedRoute, + ParamMap, + Router, + RouterLink, +} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { + combineLatest, + Observable, + of, +} from 'rxjs'; +import { + map, + mergeMap, + switchMap, + take, +} from 'rxjs/operators'; + +import { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../collection-page/collection-page.resolver'; +import { AuditDataService } from '../../core/audit/audit-data.service'; +import { Audit } from '../../core/audit/model/audit.model'; +import { AuthService } from '../../core/auth/auth.service'; +import { SortDirection } from '../../core/cache/models/sort-options.model'; +import { CollectionDataService } from '../../core/data/collection-data.service'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { FindListOptions } from '../../core/data/find-list-options.model'; +import { ItemDataService } from '../../core/data/item-data.service'; +import { PaginatedList } from '../../core/data/paginated-list.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { redirectOn4xx } from '../../core/shared/authorized.operators'; +import { Collection } from '../../core/shared/collection.model'; +import { Item } from '../../core/shared/item.model'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { PaginationComponent } from '../../shared/pagination/pagination.component'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { VarDirective } from '../../shared/utils/var.directive'; + +/** + * Component displaying a list of all audit about a object in a paginated table + */ +@Component({ + selector: 'ds-object-audit-overview', + templateUrl: './object-audit-overview.component.html', + imports: [ + PaginationComponent, + NgIf, + AsyncPipe, + TranslateModule, + NgForOf, + VarDirective, + RouterLink, + DatePipe, + ], + standalone: true, +}) +export class ObjectAuditOverviewComponent implements OnInit { + + /** + * The object extracted from the route. + */ + object: Item; + + /** + * List of all audits + */ + auditsRD$: Observable>>; + + /** + * The current pagination configuration for the page used by the FindAll method + */ + config: FindListOptions = Object.assign(new FindListOptions(), { + elementsPerPage: 10, + sort: { + field: 'timeStamp', + direction: SortDirection.DESC, + }, + }); + + /** + * The current pagination configuration for the page + */ + pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'oop', + pageSize: 10, + }); + + /** + * Date format to use for start and end time of audits + */ + dateFormat = 'yyyy-MM-dd HH:mm:ss'; + + owningCollection$: Observable; + + constructor(protected authService: AuthService, + protected route: ActivatedRoute, + protected router: Router, + protected auditService: AuditDataService, + protected itemService: ItemDataService, + protected authorizationService: AuthorizationDataService, + protected paginationService: PaginationService, + protected collectionDataService: CollectionDataService, + ) {} + + ngOnInit(): void { + this.route.paramMap.pipe( + mergeMap((paramMap: ParamMap) => this.itemService.findById(paramMap.get('objectId'))), + getFirstCompletedRemoteData(), + redirectOn4xx(this.router, this.authService), + ).subscribe((rd) => { + this.object = rd.payload; + this.owningCollection$ = this.collectionDataService.findOwningCollectionFor( + this.object, + true, + false, + ...COLLECTION_PAGE_LINKS_TO_FOLLOW, + ).pipe( + getFirstCompletedRemoteData(), + map(data => data?.payload), + ); + this.setAudits(); + }); + } + + /** + * Send a request to fetch all audits for the current page + */ + setAudits() { + const config$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config); + const isAdmin$ = this.isCurrentUserAdmin(); + const parentCommunity$ = this.owningCollection$.pipe( + switchMap(collection => collection.parentCommunity), + getFirstCompletedRemoteData(), + map(data => data?.payload), + ); + + this.auditsRD$ = combineLatest([isAdmin$, config$, this.owningCollection$, parentCommunity$]).pipe( + mergeMap(([isAdmin, config, owningCollection, parentCommunity]) => { + if (isAdmin) { + return this.auditService.findByObject(this.object.id, config, owningCollection.id, parentCommunity.id); + } + + return of(null); + }), + ); + } + + isCurrentUserAdmin(): Observable { + return combineLatest([ + this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin), + this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin), + this.authorizationService.isAuthorized(FeatureID.AdministratorOf), + ]).pipe( + map(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin]) => { + return isCollectionAdmin || isCommunityAdmin || isSiteAdmin; + }), + take(1), + ); + } + + /** + * Get the name of an EPerson by ID + * @param audit Audit object + */ + getEpersonName(audit: Audit): Observable { + return this.auditService.getEpersonName(audit); + } + + getOtherObject(audit: Audit, contextObjectId: string): Observable { + return this.auditService.getOtherObject(audit, contextObjectId); + } + +} diff --git a/src/app/audit-page/overview/audit-overview.component.html b/src/app/audit-page/overview/audit-overview.component.html new file mode 100644 index 00000000000..03c04666a0a --- /dev/null +++ b/src/app/audit-page/overview/audit-overview.component.html @@ -0,0 +1,47 @@ +
+
+

{{'audit.overview.title' | translate}}

+
+ + +
+ No audits found. +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'audit.overview.table.id' | translate }}{{ 'audit.overview.table.entityType' | translate }}{{ 'audit.overview.table.objectUUID' | translate }}{{ 'audit.overview.table.objectType' | translate }}{{ 'audit.overview.table.subjectUUID' | translate }}{{ 'audit.overview.table.subjectType' | translate }}{{ 'audit.overview.table.eperson' | translate }}{{ 'audit.overview.table.timestamp' | translate }}
{{audit.id}}{{ audit.eventType }}{{audit.objectUUID}}{{ audit.objectType }}{{ audit.subjectUUID }}{{ audit.subjectType }}{{ePersonName}}{{ audit.timeStamp | date:dateFormat}}
+
+
+
+ +
diff --git a/src/app/audit-page/overview/audit-overview.component.spec.ts b/src/app/audit-page/overview/audit-overview.component.spec.ts new file mode 100644 index 00000000000..1446ca60525 --- /dev/null +++ b/src/app/audit-page/overview/audit-overview.component.spec.ts @@ -0,0 +1,153 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; + +import { AuditDataService } from '../../core/audit/audit-data.service'; +import { Audit } from '../../core/audit/model/audit.model'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { PaginationComponent } from '../../shared/pagination/pagination.component'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { AuditMock } from '../../shared/testing/audit.mock'; +import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; +import { createPaginatedList } from '../../shared/testing/utils.test'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { AuditOverviewComponent } from './audit-overview.component'; + +describe('AuditOverviewComponent', () => { + let component: AuditOverviewComponent; + let fixture: ComponentFixture; + + let auditService: AuditDataService; + let authorizationService: any; + let audits: Audit[]; + const paginationService = new PaginationServiceStub(); + + function init() { + audits = [ AuditMock, AuditMock, AuditMock ]; + auditService = jasmine.createSpyObj('processService', { + findAll: createSuccessfulRemoteDataObject$(createPaginatedList(audits)), + getEpersonName: of('Eperson Name'), + }); + authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']); + } + + beforeEach(waitForAsync(() => { + init(); + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), VarDirective, AuditOverviewComponent], + providers: [ + { provide: AuditDataService, useValue: auditService }, + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: PaginationService, useValue: paginationService }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).overrideComponent(AuditOverviewComponent, { remove: { imports: [PaginationComponent] } }).compileComponents(); + })); + + describe('if the current user is an admin', () => { + + beforeEach(() => { + authorizationService.isAuthorized.and.callFake(() => of(true)); + + fixture = TestBed.createComponent(AuditOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('table structure', () => { + let rowElements; + + beforeEach(() => { + rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); + }); + + it(`should contain 3 rows`, () => { + expect(rowElements.length).toEqual(3); + }); + + it('should display the audit IDs in the first column', () => { + rowElements.forEach((rowElement, index) => { + const el = rowElement.query(By.css('td:nth-child(1)')).nativeElement; + expect(el.textContent).toContain(audits[index].id); + }); + }); + + it('should display the entityType in the second column', () => { + rowElements.forEach((rowElement, index) => { + const el = rowElement.query(By.css('td:nth-child(2)')).nativeElement; + expect(el.textContent).toContain(audits[index].eventType); + }); + }); + + it('should display the objectUUID in the third column', () => { + rowElements.forEach((rowElement, index) => { + const el = rowElement.query(By.css('td:nth-child(3)')).nativeElement; + expect(el.textContent).toContain(audits[index].objectUUID); + }); + }); + + it('should display the objectType in the fourth column', () => { + rowElements.forEach((rowElement, index) => { + const el = rowElement.query(By.css('td:nth-child(4)')).nativeElement; + expect(el.textContent).toContain(audits[index].objectType); + }); + }); + + it('should display the subjectUUID in the fifth column', () => { + rowElements.forEach((rowElement, index) => { + const el = rowElement.query(By.css('td:nth-child(5)')).nativeElement; + expect(el.textContent).toContain(audits[index].subjectUUID); + }); + }); + + it('should display the subjectType in the sixth column', () => { + rowElements.forEach((rowElement, index) => { + const el = rowElement.query(By.css('td:nth-child(6)')).nativeElement; + expect(el.textContent).toContain(audits[index].subjectType); + }); + }); + + it('should display the eperson name in the seventh column', () => { + rowElements.forEach((rowElement, index) => { + const el = rowElement.query(By.css('td:nth-child(7)')).nativeElement; + expect(el.textContent).toContain('Eperson Name'); + }); + }); + + }); + + }); + + describe('if the current user is not an admin', () => { + + beforeEach(() => { + authorizationService.isAuthorized.and.callFake(() => of(false)); + + fixture = TestBed.createComponent(AuditOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('table structure', () => { + let rowElements; + + beforeEach(() => { + rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); + }); + + it(`should contain 0 rows`, () => { + expect(rowElements.length).toEqual(0); + }); + + }); + }); + +}); diff --git a/src/app/audit-page/overview/audit-overview.component.ts b/src/app/audit-page/overview/audit-overview.component.ts new file mode 100644 index 00000000000..b1994852944 --- /dev/null +++ b/src/app/audit-page/overview/audit-overview.component.ts @@ -0,0 +1,127 @@ +import { + AsyncPipe, + DatePipe, + NgForOf, + NgIf, +} from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { + combineLatest, + Observable, +} from 'rxjs'; +import { mergeMap } from 'rxjs/operators'; + +import { AuditDataService } from '../../core/audit/audit-data.service'; +import { Audit } from '../../core/audit/model/audit.model'; +import { SortDirection } from '../../core/cache/models/sort-options.model'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { FindListOptions } from '../../core/data/find-list-options.model'; +import { PaginatedList } from '../../core/data/paginated-list.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { PaginationComponent } from '../../shared/pagination/pagination.component'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { VarDirective } from '../../shared/utils/var.directive'; + +/** + * Component displaying a list of all audit in a paginated table + */ +@Component({ + selector: 'ds-audit-overview', + templateUrl: './audit-overview.component.html', + imports: [ + PaginationComponent, + NgIf, + AsyncPipe, + TranslateModule, + RouterLink, + NgForOf, + VarDirective, + DatePipe, + ], + standalone: true, +}) +export class AuditOverviewComponent implements OnInit { + + /** + * List of all audits + */ + auditsRD$: Observable>>; + + /** + * Whether user is admin + */ + isAdmin$: Observable; + + /** + * The current pagination configuration for the page used by the FindAll method + */ + config: FindListOptions = Object.assign(new FindListOptions(), { + elementsPerPage: 10, + sort: { + field: 'timeStamp', + direction: SortDirection.DESC, + }, + }); + + /** + * The pagination id + */ + pageId = 'aop'; + + /** + * The current pagination configuration for the page + */ + pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: this.pageId, + pageSize: 10, + }); + + /** + * Date format to use for start and end time of audits + */ + dateFormat = 'yyyy-MM-dd HH:mm:ss'; + + constructor(protected auditService: AuditDataService, + protected authorizationService: AuthorizationDataService, + protected paginationService: PaginationService) { + } + + ngOnInit(): void { + this.setAudits(); + } + + /** + * Send a request to fetch all audits for the current page + */ + setAudits() { + const config$ = this.paginationService.getFindListOptions(this.pageId, this.config); + this.isAdmin$ = this.isCurrentUserAdmin(); + this.auditsRD$ = combineLatest([this.isAdmin$, config$]).pipe( + mergeMap(([isAdmin, config]) => { + if (isAdmin) { + return this.auditService.findAll(config); + } + }), + ); + } + + isCurrentUserAdmin(): Observable { + return this.authorizationService.isAuthorized(FeatureID.AdministratorOf, undefined, undefined); + } + + /** + * Get the name of an EPerson by ID + * @param audit Audit object + */ + getEpersonName(audit: Audit): Observable { + return this.auditService.getEpersonName(audit); + } + +} diff --git a/src/app/core/audit/audit-data.service.spec.ts b/src/app/core/audit/audit-data.service.spec.ts new file mode 100644 index 00000000000..62dc47e4e2f --- /dev/null +++ b/src/app/core/audit/audit-data.service.spec.ts @@ -0,0 +1,220 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { + Store, + StoreModule, +} from '@ngrx/store'; +import { provideMockStore } from '@ngrx/store/testing'; +import { + TranslateLoader, + TranslateModule, +} from '@ngx-translate/core'; + +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { AuditMock } from '../../shared/testing/audit.mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { + createPaginatedList, + createRequestEntry$, +} from '../../shared/testing/utils.test'; +import { followLink } from '../../shared/utils/follow-link-config.model'; +import { RequestParam } from '../cache/models/request-param.model'; +import { CoreState } from '../core-state.model'; +import { FindListOptions } from '../data/find-list-options.model'; +import { RequestService } from '../data/request.service'; +import { + AUDIT_FIND_BY_OBJECT_SEARCH_METHOD, + AuditDataService, +} from './audit-data.service'; + +describe('AuditDataService', () => { + let service: AuditDataService; + let store: Store; + let requestService: RequestService; + + let audit; + let audits; + + let restEndpointURL; + let auditsEndpoint; + let halService: any; + let paginatedAudits$; + let audit$; + + function initTestService() { + return new AuditDataService( + requestService, + null, + null, + halService, + null, + null, + ); + } + + function init() { + restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/system/auditevents'; + auditsEndpoint = `${restEndpointURL}/auditevents`; + audit = AuditMock; + audits = [AuditMock]; + audit$ = createSuccessfulRemoteDataObject$(audit); + paginatedAudits$ = createSuccessfulRemoteDataObject$(createPaginatedList(audits)); + halService = new HALEndpointServiceStub(restEndpointURL); + + TestBed.configureTestingModule({ + imports: [ + CommonModule, + StoreModule.forRoot({}), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock, + }, + }), + ], + providers: [ + provideMockStore(), + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }); + } + + beforeEach(() => { + init(); + requestService = getMockRequestService(createRequestEntry$(audits)); + store = TestBed.inject(Store); // Use TestBed.inject to get the mock store + service = initTestService(); + spyOn(store, 'dispatch'); + }); + + describe('findByObject', () => { + beforeEach(() => { + spyOn((service as any).searchData, 'searchBy').and.returnValue(paginatedAudits$); + }); + + it('should call searchBy with the objectId and follow eperson link', (done) => { + const objectId = 'objectId'; + const options = new FindListOptions(); + options.searchParams = [new RequestParam('object', objectId)]; + service.findByObject(objectId).subscribe((result) => { + expect(result.payload.page).toEqual(audits); + expect((service as any).searchData.searchBy).toHaveBeenCalledWith( + AUDIT_FIND_BY_OBJECT_SEARCH_METHOD, + options, + true, + true, + followLink('eperson'), + ); + done(); + }); + }); + }); + + + describe('findById', () => { + beforeEach(() => { + spyOn(service, 'findById').and.returnValue(audit$); + }); + + it('should call findById with id and linksToFollow', (done) => { + const linksToFollow: any = 'linksToFollow'; + service.findById(audit.id, true, true, linksToFollow).subscribe((result) => { + expect(result.payload).toEqual(audit); + expect(service.findById).toHaveBeenCalledWith(audit.id, true, true, linksToFollow); + done(); + }); + }); + }); + + describe('findAll', () => { + beforeEach(() => { + spyOn((service as any).findAllData, 'findAll').and.returnValue(paginatedAudits$); + }); + + it('should call findAll with with paginated options and followLinks', (done) => { + const linksToFollow: any = 'linksToFollow'; + const options = new FindListOptions(); + service.findAll(options, true, true, linksToFollow).subscribe((result) => { + expect(result.payload.page).toEqual(audits); + expect((service as any).findAllData.findAll).toHaveBeenCalledWith(options, true, true, linksToFollow); + done(); + }); + }); + }); + + describe('getOtherObject', () => { + beforeEach(() => { + spyOn(service, 'findByHref').and.returnValue(audit$); + }); + + it('should call findByHref it otherObjectHref exists', (done) => { + spyOn(service, 'getOtherObjectHref').and.returnValue('otherObjectHref'); + service.getOtherObject(audit, 'contextObjectId').subscribe((result) => { + expect(service.getOtherObjectHref).toHaveBeenCalledWith(audit, 'contextObjectId'); + expect(service.findByHref).toHaveBeenCalledWith('otherObjectHref'); + expect(result).toBe(audit); + done(); + }); + }); + + it('should return observable null if otherObjectHref not exists', (done) => { + spyOn(service, 'getOtherObjectHref').and.returnValue(null); + service.getOtherObject(audit, 'contextObjectId').subscribe((result) => { + expect(service.getOtherObjectHref).toHaveBeenCalledWith(audit, 'contextObjectId'); + expect(service.findByHref).not.toHaveBeenCalled(); + expect(result).toBe(null); + done(); + }); + }); + }); + + describe('getOtherObjectHref', () => { + + it('should return the proper other object href if exists', () => { + let otherObjectHref; + let testAudit; + const contextObject = 'contextObject'; + + // if audit.objectUUID has no value return null + testAudit = { + objectUUID: null, + }; + otherObjectHref = service.getOtherObjectHref(testAudit, contextObject); + expect(otherObjectHref).toBe(null); + + // if contextObject equals to audit.objectUUID return subjectHref + testAudit = { + objectUUID: 'contextObject', + subjectUUID: 'subjectUUID', + _links: { subject: { href: 'subjectHref' } }, + }; + otherObjectHref = service.getOtherObjectHref(testAudit, contextObject); + expect(otherObjectHref).toBe('subjectHref'); + + // if contextObject equals to audit.subjectUUID return objectHref + testAudit = { + objectUUID: 'objectUUID', + subjectUUID: 'contextObject', + _links: { object: { href: 'objectHref' } }, + }; + otherObjectHref = service.getOtherObjectHref(testAudit, contextObject); + expect(otherObjectHref).toBe('objectHref'); + + // if contextObject not equals to audit.subjectUUID and audit.objectUUID return null; + testAudit = { + objectUUID: 'objectUUID', + subjectUUID: 'subjectUUID', + _links: { subject: { href: 'subjectHref' } }, + }; + otherObjectHref = service.getOtherObjectHref(testAudit, contextObject); + expect(otherObjectHref).toBe(null); + + }); + }); + + +}); + diff --git a/src/app/core/audit/audit-data.service.ts b/src/app/core/audit/audit-data.service.ts new file mode 100644 index 00000000000..51506a13061 --- /dev/null +++ b/src/app/core/audit/audit-data.service.ts @@ -0,0 +1,159 @@ +import { Injectable } from '@angular/core'; +import { + Observable, + of, +} from 'rxjs'; +import { + map, + startWith, +} from 'rxjs/operators'; + +import { hasValue } from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { + followLink, + FollowLinkConfig, +} from '../../shared/utils/follow-link-config.model'; +import { DSONameService } from '../breadcrumbs/dso-name.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { RequestParam } from '../cache/models/request-param.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { DeleteDataImpl } from '../data/base/delete-data'; +import { + FindAllData, + FindAllDataImpl, +} from '../data/base/find-all-data'; +import { IdentifiableDataService } from '../data/base/identifiable-data.service'; +import { SearchDataImpl } from '../data/base/search-data'; +import { FindListOptions } from '../data/find-list-options.model'; +import { PaginatedList } from '../data/paginated-list.model'; +import { RemoteData } from '../data/remote-data'; +import { RequestService } from '../data/request.service'; +import { EPerson } from '../eperson/models/eperson.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { + getFirstSucceededRemoteDataPayload, + getFirstSucceededRemoteDataWithNotEmptyPayload, +} from '../shared/operators'; +import { Audit } from './model/audit.model'; + +export const AUDIT_PERSON_NOT_AVAILABLE = 'n/a'; + +export const AUDIT_FIND_BY_OBJECT_SEARCH_METHOD = 'findByObject'; + +@Injectable({ providedIn: 'root' }) +export class AuditDataService extends IdentifiableDataService{ + + private searchData: SearchDataImpl; + private findAllData: FindAllData; + private deleteData: DeleteDataImpl; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected dsoNameService: DSONameService, + ) { + super('auditevents', 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); + } + + /** + * Get all audit event for the object. + * + * @param objectId The objectId id + * @param options The [[FindListOptions]] object + * @param collUuid The Uuid of the collection + * @param commUuid The Uuid of the community + * @return Observable>> + */ + findByObject(objectId: string, options: FindListOptions = {}, collUuid?: string, commUuid?: string): Observable>> { + const searchMethod = AUDIT_FIND_BY_OBJECT_SEARCH_METHOD; + const searchParams = [new RequestParam('object', objectId)]; + + if (hasValue(commUuid)) { + searchParams.push(new RequestParam('commUuid', commUuid)); + } + + if (hasValue(collUuid)) { + searchParams.push(new RequestParam('collUuid', collUuid)); + } + const optionsWithObject = Object.assign(new FindListOptions(), options, { + searchParams, + }); + return this.searchData.searchBy(searchMethod, optionsWithObject, true, true, followLink('eperson')); + } + + /** + * Returns an observable of {@link RemoteData} of an object, based on its ID, with a list of + * {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object + * @param id ID of object we want to retrieve + * @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 + */ + findById(id: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return super.findById(id, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + findAll(options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + /** + * Get the name of an EPerson by ID + * @param audit The audit object + */ + getEpersonName(audit: Audit): Observable { + + if (!audit.epersonUUID) { + return of(AUDIT_PERSON_NOT_AVAILABLE); + } + + // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved + return audit.eperson.pipe( + getFirstSucceededRemoteDataWithNotEmptyPayload(), + map((eperson: EPerson) => this.dsoNameService.getName(eperson)), + startWith(AUDIT_PERSON_NOT_AVAILABLE)); + } + + /** + * + * @param audit + * @param contextObjectId + */ + getOtherObject(audit: Audit, contextObjectId: string): Observable { + const otherObjectHref = this.getOtherObjectHref(audit, contextObjectId); + + if (otherObjectHref) { + return this.findByHref(otherObjectHref).pipe( + getFirstSucceededRemoteDataPayload(), + ); + } + return of(null); + } + + getOtherObjectHref(audit: Audit, contextObjectId: string): string { + if (audit.objectUUID === null) { + return null; + } + if (contextObjectId === audit.objectUUID) { + // other object is on the subject field + return audit._links.subject.href; + } else if (contextObjectId === audit.subjectUUID) { + // other object is on the object field + return audit._links.object.href; + } else { + return null; + } + } + +} diff --git a/src/app/core/audit/model/audit.model.ts b/src/app/core/audit/model/audit.model.ts new file mode 100644 index 00000000000..e476c3bd3e5 --- /dev/null +++ b/src/app/core/audit/model/audit.model.ts @@ -0,0 +1,121 @@ +import { + autoserialize, + deserialize, +} from 'cerialize'; +import { Observable } from 'rxjs'; + +import { + link, + typedObject, +} from '../../cache/builders/build-decorators'; +import { CacheableObject } from '../../cache/cacheable-object.model'; +import { RemoteData } from '../../data/remote-data'; +import { EPerson } from '../../eperson/models/eperson.model'; +import { EPERSON } from '../../eperson/models/eperson.resource-type'; +import { DSpaceObject } from '../../shared/dspace-object.model'; +import { HALLink } from '../../shared/hal-link.model'; +import { ITEM } from '../../shared/item.resource-type'; +import { ResourceType } from '../../shared/resource-type'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; +import { AUDIT } from './audit.resource-type'; + +/** + * Object representing an Audit. + */ +@typedObject +export class Audit implements CacheableObject { + static type = AUDIT; + + /** + * The object type + */ + @excludeFromEquals + @autoserialize + type: ResourceType; + + /** + * The identifier for this audit + */ + @autoserialize + id: string; + + /** + * The eperson UUID for this audit + */ + @autoserialize + epersonUUID: string; + + /** + * The subject UUID for this audit + */ + @autoserialize + subjectUUID: string; + + /** + * The subject type for this audit + */ + @autoserialize + subjectType: string; + + /** + * The object UUID for this audit + */ + @autoserialize + objectUUID: string; + + /** + * The object type for this audit + */ + @autoserialize + objectType: string; + + /** + * The detail for this audit + */ + @autoserialize + detail: string; + + /** + * The eventType for this audit + */ + @autoserialize + eventType: string; + + /** + * The timestamp for this audit + */ + @autoserialize + timeStamp: string; + + /** + * The {@link HALLink}s for this Audit + */ + @deserialize + _links: { + self: HALLink; + eperson: HALLink; + subject: HALLink; + object: HALLink; + }; + + /** + * The EPerson for this audit + * Will be undefined unless the eperson {@link HALLink} has been resolved. + */ + @link(EPERSON, false) + eperson?: Observable>; + + /** + * The Subject for this audit + * Will be undefined unless the subject {@link HALLink} has been resolved. + */ + @link(ITEM) + subject?: Observable>; + + /** + * The Object for this audit + * Will be undefined unless the object {@link HALLink} has been resolved. + */ + @link(ITEM) + object?: Observable>; +} diff --git a/src/app/core/audit/model/audit.resource-type.ts b/src/app/core/audit/model/audit.resource-type.ts new file mode 100644 index 00000000000..27fb6378891 --- /dev/null +++ b/src/app/core/audit/model/audit.resource-type.ts @@ -0,0 +1,9 @@ +/** + * The resource type for Process + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +import { ResourceType } from '../../shared/resource-type'; + +export const AUDIT = new ResourceType('auditevent'); diff --git a/src/app/core/data-services-map.ts b/src/app/core/data-services-map.ts index c9ebbc5ffc3..01c3ab5487f 100644 --- a/src/app/core/data-services-map.ts +++ b/src/app/core/data-services-map.ts @@ -13,6 +13,7 @@ import { IDENTIFIERS } from '../shared/object-list/identifier-data/identifier-da import { SUBSCRIPTION } from '../shared/subscriptions/models/subscription.resource-type'; import { SUBMISSION_COAR_NOTIFY_CONFIG } from '../submission/sections/section-coar-notify/section-coar-notify-service.resource-type'; import { SYSTEMWIDEALERT } from '../system-wide-alert/system-wide-alert.resource-type'; +import { AUDIT } from './audit/model/audit.resource-type'; import { BULK_ACCESS_CONDITION_OPTIONS, SUBMISSION_ACCESSES_TYPE, @@ -136,4 +137,5 @@ export const LAZY_DATA_SERVICES: LazyDataServicesMap = new Map([ [SUGGESTION_TARGET.value, () => import('./notifications/suggestions/target/suggestion-target-data.service').then(m => m.SuggestionTargetDataService)], [DUPLICATE.value, () => import('./submission/submission-duplicate-data.service').then(m => m.SubmissionDuplicateDataService)], [CorrectionType.type.value, () => import('./submission/correctiontype-data.service').then(m => m.CorrectionTypeDataService)], + [AUDIT.value, () => import('./audit/audit-data.service').then(m => m.AuditDataService)], ]); diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index b2d5476d21a..8a02b467510 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -282,9 +282,12 @@ export class CollectionDataService extends ComColDataService { /** * Returns {@link RemoteData} of {@link Collection} that is the owning collection of the given item * @param item Item we want the owning collection of + * @param useCachedVersionIfAvailable + * @param reRequestOnStale + * @param linksToFollow */ - findOwningCollectionFor(item: Item): Observable> { - return this.findByHref(item._links.owningCollection.href); + findOwningCollectionFor(item: Item, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.findByHref(item._links.owningCollection.href, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** diff --git a/src/app/core/provide-core.ts b/src/app/core/provide-core.ts index 0057c0823d7..f3b4c441a4e 100644 --- a/src/app/core/provide-core.ts +++ b/src/app/core/provide-core.ts @@ -17,6 +17,7 @@ import { IdentifierData } from '../shared/object-list/identifier-data/identifier import { Subscription } from '../shared/subscriptions/models/subscription.model'; import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config'; import { SystemWideAlert } from '../system-wide-alert/system-wide-alert.model'; +import { Audit } from './audit/model/audit.model'; import { AuthStatus } from './auth/models/auth-status.model'; import { ShortLivedToken } from './auth/models/short-lived-token.model'; import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model'; @@ -187,4 +188,5 @@ export const models = SubmissionCoarNotifyConfig, NotifyRequestsStatus, SystemWideAlert, + Audit, ]; diff --git a/src/app/shared/menu/providers/audit-item.menu.spec.ts b/src/app/shared/menu/providers/audit-item.menu.spec.ts new file mode 100644 index 00000000000..7efe71d480d --- /dev/null +++ b/src/app/shared/menu/providers/audit-item.menu.spec.ts @@ -0,0 +1,45 @@ + +import { Collection } from '../../../core/shared/collection.model'; +import { COLLECTION } from '../../../core/shared/collection.resource-type'; +import { URLCombiner } from '../../../core/url-combiner/url-combiner'; +import { MenuItemType } from '../menu-item-type.model'; +import { PartialMenuSection } from '../menu-provider.model'; +import { AuditLogsMenuProvider } from './audit-item.menu'; + +describe('AuditLogsMenuProvider', () => { + + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'context-menu.actions.audit-item.btn', + link: new URLCombiner('/auditlogs/object').toString(), + }, + icon: 'pencil-alt', + }, + ]; + + let provider: AuditLogsMenuProvider; + + const dso: Collection = Object.assign(new Collection(), { + type: COLLECTION.value, + uuid: 'test-uuid', + _links: { self: { href: 'self-link' } }, + }); + + it('should be created', () => { + expect(provider).toBeTruthy(); + }); + + describe('getSectionsForContext', () => { + it('should return the expected sections', (done) => { + provider.getSectionsForContext(dso).subscribe((sections) => { + expect(sections).toEqual(expectedSections); + done(); + }); + }); + }); + + +}); diff --git a/src/app/shared/menu/providers/audit-item.menu.ts b/src/app/shared/menu/providers/audit-item.menu.ts new file mode 100644 index 00000000000..c4527173f25 --- /dev/null +++ b/src/app/shared/menu/providers/audit-item.menu.ts @@ -0,0 +1,55 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { Injectable } from '@angular/core'; +import { + map, + Observable, +} from 'rxjs'; + +import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { LinkMenuItemModel } from '../menu-item/models/link.model'; +import { MenuItemType } from '../menu-item-type.model'; +import { PartialMenuSection } from '../menu-provider.model'; +import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu'; + +/** + * Menu provider to create the "Audit" option in the DSO audit menu + */ +@Injectable() +export class AuditLogsMenuProvider extends DSpaceObjectPageMenuProvider { + constructor( + protected authorizationDataService: AuthorizationDataService, + protected configurationDataService: ConfigurationDataService, + ) { + super(); + } + + public getSectionsForContext(dso: DSpaceObject): Observable { + return this.configurationDataService.findByPropertyName('context-menu-entry.audit.enabled').pipe( + getFirstCompletedRemoteData(), + map((rd) => { + return rd.hasSucceeded ? rd.payload.values.length > 0 && rd.payload.values[0] === 'true' : false; + debugger; + }), + map((isAuditEnabled) => { + return [{ + model: { + type: MenuItemType.TEXT, + text: 'context-menu.actions.audit-item.btn', + link: '/auditlogs/object', + } as LinkMenuItemModel, + icon: 'key', + visible: true, + }]; + }), + ); + } +} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 56fd637ff3f..ab4eee9f8bb 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -892,6 +892,50 @@ "admin.batch-import.page.remove": "remove", + + "audit.overview.title": "Audit Logs Overview", + + "audit.overview.table.id": "Audit ID", + + "audit.overview.table.objectUUID": "Object ID", + + "audit.overview.table.objectType": "Object Type", + + "audit.overview.table.subjectUUID": "Subject ID", + + "audit.overview.table.subjectType": "Subject Type", + + "audit.overview.table.entityType": "Audit Type", + + "audit.overview.table.eperson": "EPerson", + + "audit.overview.table.timestamp": "Time", + + "audit.overview.breadcrumbs": "Audit Logs Overview", + + "audit.object.overview.title": "Subject Audit Logs Overview", + + "audit.object.overview.disabled.message": "Audit feature is currently disabled", + + "audit.detail.title": "Audit Detail", + + "audit.detail.id": "Audit Id", + + "audit.detail.subjectUUID": "Subject ID", + + "audit.detail.subjectType": "Subject Type", + + "audit.detail.eventType": "Audit Type", + + "audit.detail.eperson": "EPerson", + + "audit.detail.timeStamp": "Time", + + "audit.detail.back": "All Audit Logs", + + "audit.detail.back.subject": "Subject Audit Logs", + + "auth.errors.invalid-user": "Invalid email address or password.", "auth.messages.expired": "Your session has expired. Please log in again.", @@ -1596,6 +1640,8 @@ "community.sub-community-list.head": "Communities in this Community", + "context-menu.actions.audit-item.btn": "Audit", + "cookies.consent.accept-all": "Accept all", "cookies.consent.accept-selected": "Accept selected", From 78de62bc47b126def5e7af906d211fd02e162440 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Tue, 15 Jul 2025 17:05:28 +0200 Subject: [PATCH 02/22] [DURACOM-317] fix menu, eperson resolution, add config for overview, fix tests --- config/config.example.yml | 3 + src/app/app.menus.ts | 6 +- .../object-audit-overview.component.ts | 7 ++- src/app/core/audit/audit-data.service.ts | 4 +- .../menu/providers/audit-item.menu.spec.ts | 2 +- .../shared/menu/providers/audit-item.menu.ts | 29 +++++---- .../providers/audit-overview.menu.spec.ts | 59 +++++++++++++++++++ .../menu/providers/audit-overview.menu.ts | 57 ++++++++++++++++++ src/assets/i18n/en.json5 | 2 + src/config/app-config.interface.ts | 1 + src/config/default-app-config.ts | 2 + src/environments/environment.test.ts | 2 + 12 files changed, 157 insertions(+), 17 deletions(-) create mode 100644 src/app/shared/menu/providers/audit-overview.menu.spec.ts create mode 100644 src/app/shared/menu/providers/audit-overview.menu.ts diff --git a/config/config.example.yml b/config/config.example.yml index d5e7dfe5016..ca5d7b90e5d 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -593,3 +593,6 @@ geospatialMapViewer: defaultCentrePoint: lat: 41.015137 lng: 28.979530 +# Option to enable the menu entry to see audit logs on a site level. +# Only site administrators will be able to use this functionality +enableAuditLogsOverview: true diff --git a/src/app/app.menus.ts b/src/app/app.menus.ts index 112df24a8ca..cccabeb4f1a 100644 --- a/src/app/app.menus.ts +++ b/src/app/app.menus.ts @@ -34,6 +34,7 @@ import { StatisticsMenuProvider } from './shared/menu/providers/statistics.menu' import { SystemWideAlertMenuProvider } from './shared/menu/providers/system-wide-alert.menu'; import { WithdrawnReinstateItemMenuProvider } from './shared/menu/providers/withdrawn-reinstate-item.menu'; import { WorkflowMenuProvider } from './shared/menu/providers/workflow.menu'; +import { AuditOverviewMenuProvider } from "./shared/menu/providers/audit-overview.menu"; /** * Represents and builds the menu structure for the three available menus (public navbar, admin sidebar and the dso edit @@ -73,7 +74,7 @@ export const MENUS = buildMenuStructure({ HealthMenuProvider, SystemWideAlertMenuProvider, CoarNotifyMenuProvider, - AuditLogsMenuProvider, + AuditOverviewMenuProvider ], [MenuID.DSO_EDIT]: [ DsoOptionMenuProvider.withSubs([ @@ -92,6 +93,9 @@ export const MENUS = buildMenuStructure({ VersioningMenuProvider.onRoute( MenuRoute.ITEM_PAGE, ), + AuditLogsMenuProvider.onRoute( + MenuRoute.ITEM_PAGE, + ), OrcidMenuProvider.onRoute( MenuRoute.ITEM_PAGE, ), diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts index 6e7962a99f9..0338a6fe006 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts @@ -24,7 +24,7 @@ import { map, mergeMap, switchMap, - take, + take, tap, } from 'rxjs/operators'; import { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../collection-page/collection-page.resolver'; @@ -149,7 +149,10 @@ export class ObjectAuditOverviewComponent implements OnInit { this.auditsRD$ = combineLatest([isAdmin$, config$, this.owningCollection$, parentCommunity$]).pipe( mergeMap(([isAdmin, config, owningCollection, parentCommunity]) => { if (isAdmin) { - return this.auditService.findByObject(this.object.id, config, owningCollection.id, parentCommunity.id); + return this.auditService.findByObject(this.object.id, config, owningCollection.id, parentCommunity.id).pipe( + getFirstCompletedRemoteData(), + tap(console.log) + ); } return of(null); diff --git a/src/app/core/audit/audit-data.service.ts b/src/app/core/audit/audit-data.service.ts index 51506a13061..0a68b6e946a 100644 --- a/src/app/core/audit/audit-data.service.ts +++ b/src/app/core/audit/audit-data.service.ts @@ -113,8 +113,8 @@ export class AuditDataService extends IdentifiableDataService{ * @param audit The audit object */ getEpersonName(audit: Audit): Observable { - - if (!audit.epersonUUID) { + // TODO: check why person is missing + if (!audit.epersonUUID || !audit.eperson) { return of(AUDIT_PERSON_NOT_AVAILABLE); } diff --git a/src/app/shared/menu/providers/audit-item.menu.spec.ts b/src/app/shared/menu/providers/audit-item.menu.spec.ts index 7efe71d480d..9cda67bb157 100644 --- a/src/app/shared/menu/providers/audit-item.menu.spec.ts +++ b/src/app/shared/menu/providers/audit-item.menu.spec.ts @@ -16,7 +16,7 @@ describe('AuditLogsMenuProvider', () => { text: 'context-menu.actions.audit-item.btn', link: new URLCombiner('/auditlogs/object').toString(), }, - icon: 'pencil-alt', + icon: 'key', }, ]; diff --git a/src/app/shared/menu/providers/audit-item.menu.ts b/src/app/shared/menu/providers/audit-item.menu.ts index c4527173f25..5c9fefe7381 100644 --- a/src/app/shared/menu/providers/audit-item.menu.ts +++ b/src/app/shared/menu/providers/audit-item.menu.ts @@ -7,6 +7,7 @@ */ import { Injectable } from '@angular/core'; import { + combineLatest, map, Observable, } from 'rxjs'; @@ -19,6 +20,9 @@ import { LinkMenuItemModel } from '../menu-item/models/link.model'; import { MenuItemType } from '../menu-item-type.model'; import { PartialMenuSection } from '../menu-provider.model'; import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu'; +import { FeatureID } from "../../../core/data/feature-authorization/feature-id"; +import { RemoteData } from "../../../core/data/remote-data"; +import { ConfigurationProperty } from "../../../core/shared/configuration-property.model"; /** * Menu provider to create the "Audit" option in the DSO audit menu @@ -33,22 +37,25 @@ export class AuditLogsMenuProvider extends DSpaceObjectPageMenuProvider { } public getSectionsForContext(dso: DSpaceObject): Observable { - return this.configurationDataService.findByPropertyName('context-menu-entry.audit.enabled').pipe( - getFirstCompletedRemoteData(), - map((rd) => { - return rd.hasSucceeded ? rd.payload.values.length > 0 && rd.payload.values[0] === 'true' : false; - debugger; - }), - map((isAuditEnabled) => { + return combineLatest([ + this.authorizationDataService.isAuthorized(FeatureID.AdministratorOf), + this.configurationDataService.findByPropertyName('context-menu-entry.audit.enabled').pipe( + getFirstCompletedRemoteData(), + map((response: RemoteData) => { + return response.hasSucceeded ? (response.payload.values.length > 0 && response.payload.values[0] === 'true') : false; + }) + ) + ]).pipe( + map(([isAdmin, isAuditEnabled]: [boolean, boolean]) => { return [{ model: { - type: MenuItemType.TEXT, + type: MenuItemType.LINK, text: 'context-menu.actions.audit-item.btn', - link: '/auditlogs/object', + link: '/auditlogs/object/' + dso.uuid, } as LinkMenuItemModel, icon: 'key', - visible: true, - }]; + visible: isAuditEnabled && isAdmin, + }] as PartialMenuSection[]; }), ); } diff --git a/src/app/shared/menu/providers/audit-overview.menu.spec.ts b/src/app/shared/menu/providers/audit-overview.menu.spec.ts new file mode 100644 index 00000000000..15d8c06bf6f --- /dev/null +++ b/src/app/shared/menu/providers/audit-overview.menu.spec.ts @@ -0,0 +1,59 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { TestBed } from '@angular/core/testing'; +import { of as observableOf } from 'rxjs'; + +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; +import { MenuItemType } from '../menu-item-type.model'; +import { PartialMenuSection } from '../menu-provider.model'; +import { HealthMenuProvider } from './health.menu'; +import { AuditOverviewMenuProvider } from "./audit-overview.menu"; + +describe('AuditOverviewMenuProvider', () => { + const expectedSections: PartialMenuSection[] = [ + { + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.audit', + link: '/auditlogs', + }, + icon: 'key', + }, + ]; + + let provider: AuditOverviewMenuProvider; + let authorizationServiceStub = new AuthorizationDataServiceStub(); + + beforeEach(() => { + spyOn(authorizationServiceStub, 'isAuthorized').and.returnValue( + observableOf(true), + ); + + TestBed.configureTestingModule({ + providers: [ + AuditOverviewMenuProvider, + { provide: AuthorizationDataService, useValue: authorizationServiceStub }, + ], + }); + provider = TestBed.inject(AuditOverviewMenuProvider); + }); + + it('should be created', () => { + expect(provider).toBeTruthy(); + }); + + it('getSections should return expected menu sections', (done) => { + provider.getSections().subscribe((sections) => { + expect(sections).toEqual(expectedSections); + done(); + }); + }); +}); diff --git a/src/app/shared/menu/providers/audit-overview.menu.ts b/src/app/shared/menu/providers/audit-overview.menu.ts new file mode 100644 index 00000000000..4108cd7c40a --- /dev/null +++ b/src/app/shared/menu/providers/audit-overview.menu.ts @@ -0,0 +1,57 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { Inject, Injectable } from '@angular/core'; +import { + combineLatest, + map, + Observable, of, +} from 'rxjs'; + +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; +import { MenuItemType } from '../menu-item-type.model'; +import { + AbstractMenuProvider, + PartialMenuSection, +} from '../menu-provider.model'; +import { APP_CONFIG, AppConfig } from "../../../../config/app-config.interface"; + +/** + * Menu provider to create the "Health" menu in the admin sidebar + */ +@Injectable() +export class AuditOverviewMenuProvider extends AbstractMenuProvider { + constructor( + protected authorizationService: AuthorizationDataService, + @Inject(APP_CONFIG) protected appConfig: AppConfig, + ) { + console.log('AuditOverviewMenuProvider'); + super(); + } + + public getSections(): Observable { + return combineLatest([ + this.authorizationService.isAuthorized(FeatureID.AdministratorOf), + ]).pipe( + map(([isSiteAdmin]) => { + return [ + { + visible: isSiteAdmin && this.appConfig.enableAuditLogsOverview, + model: { + type: MenuItemType.LINK, + text: 'menu.section.audit', + link: '/auditlogs', + }, + icon: 'key', + }, + ] as PartialMenuSection[]; + }), + ); + } +} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index ab4eee9f8bb..d269fb59b05 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -3382,6 +3382,8 @@ "menu.section.access_control_people": "People", + "menu.section.audit": "Audit Overview", + "menu.section.reports": "Reports", "menu.section.reports.collections": "Filtered Collections", diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 94c7bd9f7da..c562227dd9e 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -70,6 +70,7 @@ interface AppConfig extends Config { liveRegion: LiveRegionConfig; matomo?: MatomoConfig; geospatialMapViewer: GeospatialMapConfig; + enableAuditLogsOverview?: boolean; } /** diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 3dbcc8a3267..f4c23a71263 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -628,4 +628,6 @@ export class DefaultAppConfig implements AppConfig { lng: 28.979530, }, }; + + enableAuditLogsOverview = true; } diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index b6348f949d5..85e9fded9ba 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -455,4 +455,6 @@ export const environment: BuildConfig = { lng: 28.979530, }, }, + + enableAuditLogsOverview: true, }; From 8d970a151242e0827c1e7f165551bd01955051e4 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Wed, 16 Jul 2025 17:54:22 +0200 Subject: [PATCH 03/22] [DURACOM-317] refactor to flow control, remove observables call from template, adjust tables references and models --- src/app/app.menus.ts | 4 +- .../detail/audit-detail.component.ts | 2 - .../object-audit-overview.component.html | 108 +++++++++--------- .../object-audit-overview.component.ts | 59 ++++++---- .../overview/audit-overview.component.html | 91 ++++++++------- .../overview/audit-overview.component.ts | 42 ++++--- src/app/core/audit/audit-data.service.ts | 7 +- src/app/core/audit/model/audit.model.ts | 10 ++ .../shared/menu/providers/audit-item.menu.ts | 10 +- .../providers/audit-overview.menu.spec.ts | 3 +- .../menu/providers/audit-overview.menu.ts | 12 +- src/assets/i18n/en.json5 | 2 + 12 files changed, 201 insertions(+), 149 deletions(-) diff --git a/src/app/app.menus.ts b/src/app/app.menus.ts index cccabeb4f1a..cd54cde1f8e 100644 --- a/src/app/app.menus.ts +++ b/src/app/app.menus.ts @@ -11,6 +11,7 @@ import { MenuRoute } from './shared/menu/menu-route.model'; import { AccessControlMenuProvider } from './shared/menu/providers/access-control.menu'; import { AdminSearchMenuProvider } from './shared/menu/providers/admin-search.menu'; import { AuditLogsMenuProvider } from './shared/menu/providers/audit-item.menu'; +import { AuditOverviewMenuProvider } from './shared/menu/providers/audit-overview.menu'; import { BrowseMenuProvider } from './shared/menu/providers/browse.menu'; import { CoarNotifyMenuProvider } from './shared/menu/providers/coar-notify.menu'; import { SubscribeMenuProvider } from './shared/menu/providers/comcol-subscribe.menu'; @@ -34,7 +35,6 @@ import { StatisticsMenuProvider } from './shared/menu/providers/statistics.menu' import { SystemWideAlertMenuProvider } from './shared/menu/providers/system-wide-alert.menu'; import { WithdrawnReinstateItemMenuProvider } from './shared/menu/providers/withdrawn-reinstate-item.menu'; import { WorkflowMenuProvider } from './shared/menu/providers/workflow.menu'; -import { AuditOverviewMenuProvider } from "./shared/menu/providers/audit-overview.menu"; /** * Represents and builds the menu structure for the three available menus (public navbar, admin sidebar and the dso edit @@ -74,7 +74,7 @@ export const MENUS = buildMenuStructure({ HealthMenuProvider, SystemWideAlertMenuProvider, CoarNotifyMenuProvider, - AuditOverviewMenuProvider + AuditOverviewMenuProvider, ], [MenuID.DSO_EDIT]: [ DsoOptionMenuProvider.withSubs([ diff --git a/src/app/audit-page/detail/audit-detail.component.ts b/src/app/audit-page/detail/audit-detail.component.ts index 29cbd8a3e96..b9e18747cf8 100644 --- a/src/app/audit-page/detail/audit-detail.component.ts +++ b/src/app/audit-page/detail/audit-detail.component.ts @@ -1,7 +1,6 @@ import { AsyncPipe, DatePipe, - NgIf, } from '@angular/common'; import { ChangeDetectionStrategy, @@ -33,7 +32,6 @@ import { VarDirective } from '../../shared/utils/var.directive'; templateUrl: './audit-detail.component.html', changeDetection: ChangeDetectionStrategy.OnPush, imports: [ - NgIf, AsyncPipe, TranslateModule, VarDirective, diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html index 84ed4707468..3518f4d5141 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html @@ -1,67 +1,67 @@
-
-

{{'audit.object.overview.title' | translate}}

-
+
+

{{'audit.object.overview.title' | translate}}

+
- @if (object) { -

{{ object.name }} ({{object.type}})

- @if ((auditsRD$ | async)?.payload; as audits) { - @if (audits.totalElements === 0) { -
- No audits found. -
- } + @if (object) { +

{{ object.name }} ({{object.type}})

+ @if ((auditsRD$ | async)?.payload; as audits) { + @if (audits.totalElements === 0) { +
+ No audits found. +
+ } - @if (audits.totalElements > 0) { - + @if (audits.totalElements > 0) { + -
- - +
+
+ - + - + - - - - - - - - - - -
{{ 'audit.overview.table.id' | translate }} {{ 'audit.overview.table.entityType' | translate }} {{ 'audit.overview.table.eperson' | translate }} {{ 'audit.overview.table.timestamp' | translate }}Other Object{{ 'audit.overview.table.other' | translate }}
{{ audit.eventType }}{{ePersonName}}{{ audit.timeStamp | date:dateFormat}} - @if (object.id === audit.objectUUID) { - - - @if ((getOtherObject(audit, object.id) | async); as subject) { - @if (subject) { - {{ subject.name }} ({{ subject.type }}) - } - } - - } -
-
-
- } - Back to Item - - - - } - @if ((auditsRD$ | async)?.statusCode === 404) { -

{{'audit.object.overview.disabled.message' | translate}}

+ + + @for (audit of audits.page; track audit) { + + {{audit.id}} + {{ audit.eventType }} + {{ audit.epersonName }} + {{ audit.timeStamp | date:dateFormat}} + + @if (object.id === audit.objectUUID) { + + @if (audit.otherAuditObject; as dso) { + {{ dsoNameService.getName(dso) }} ({{ dso.type }}) + } @else { + {{ dataNotAvailable }} + } + + } @else { + {{ dataNotAvailable }} + } + + + } + + +
+ } + Back to Item + } + @if ((auditsRD$ | async)?.statusCode === 404) { +

{{'audit.object.overview.disabled.message' | translate}}

} + } diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts index 0338a6fe006..2296756888d 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts @@ -1,8 +1,6 @@ import { AsyncPipe, DatePipe, - NgForOf, - NgIf, } from '@angular/common'; import { Component, @@ -17,20 +15,26 @@ import { import { TranslateModule } from '@ngx-translate/core'; import { combineLatest, + forkJoin, Observable, of, } from 'rxjs'; import { + filter, map, mergeMap, switchMap, - take, tap, + take, } from 'rxjs/operators'; import { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../collection-page/collection-page.resolver'; -import { AuditDataService } from '../../core/audit/audit-data.service'; +import { + AUDIT_PERSON_NOT_AVAILABLE, + AuditDataService, +} from '../../core/audit/audit-data.service'; import { Audit } from '../../core/audit/model/audit.model'; import { AuthService } from '../../core/auth/auth.service'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { SortDirection } from '../../core/cache/models/sort-options.model'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; @@ -56,10 +60,8 @@ import { VarDirective } from '../../shared/utils/var.directive'; templateUrl: './object-audit-overview.component.html', imports: [ PaginationComponent, - NgIf, AsyncPipe, TranslateModule, - NgForOf, VarDirective, RouterLink, DatePipe, @@ -104,6 +106,8 @@ export class ObjectAuditOverviewComponent implements OnInit { owningCollection$: Observable; + dataNotAvailable = AUDIT_PERSON_NOT_AVAILABLE; + constructor(protected authService: AuthService, protected route: ActivatedRoute, protected router: Router, @@ -112,6 +116,7 @@ export class ObjectAuditOverviewComponent implements OnInit { protected authorizationService: AuthorizationDataService, protected paginationService: PaginationService, protected collectionDataService: CollectionDataService, + public dsoNameService: DSONameService, ) {} ngOnInit(): void { @@ -151,13 +156,38 @@ export class ObjectAuditOverviewComponent implements OnInit { if (isAdmin) { return this.auditService.findByObject(this.object.id, config, owningCollection.id, parentCommunity.id).pipe( getFirstCompletedRemoteData(), - tap(console.log) ); } - return of(null); }), + filter(data => data && data?.payload?.page?.length > 0), + mergeMap(auditsRD => { + const updatedAudits$ = auditsRD.payload.page.map(audit => { + return forkJoin({ + epersonName: this.auditService.getEpersonName(audit), + otherAuditObject: this.auditService.getOtherObject(audit, this.object.id), + }).pipe( + map(({ epersonName, otherAuditObject }) => + Object.assign(new Audit(), audit, { epersonName, otherAuditObject }), + ), + ); + }); + + return forkJoin(updatedAudits$).pipe( + map(updatedAudits => Object.assign(new RemoteData( + auditsRD.timeCompleted, + auditsRD.msToLive, + auditsRD.lastUpdated, + auditsRD.state, + auditsRD.errorMessage, + Object.assign(new PaginatedList(), { ...auditsRD.payload, page: updatedAudits }), + auditsRD.statusCode, + ))), + ); + }), ); + + this.auditsRD$.subscribe(console.log); } isCurrentUserAdmin(): Observable { @@ -172,17 +202,4 @@ export class ObjectAuditOverviewComponent implements OnInit { take(1), ); } - - /** - * Get the name of an EPerson by ID - * @param audit Audit object - */ - getEpersonName(audit: Audit): Observable { - return this.auditService.getEpersonName(audit); - } - - getOtherObject(audit: Audit, contextObjectId: string): Observable { - return this.auditService.getOtherObject(audit, contextObjectId); - } - } diff --git a/src/app/audit-page/overview/audit-overview.component.html b/src/app/audit-page/overview/audit-overview.component.html index 03c04666a0a..f1014ce6b74 100644 --- a/src/app/audit-page/overview/audit-overview.component.html +++ b/src/app/audit-page/overview/audit-overview.component.html @@ -1,47 +1,54 @@
-
-

{{'audit.overview.title' | translate}}

-
+
+

{{'audit.overview.title' | translate}}

+
- -
- No audits found. + @if (isAdmin$ | async) { + @if ((auditsRD$ | async)?.payload?.totalElements === 0) { +
+ No audits found. +
+ } + @if ((auditsRD$ | async)?.payload?.totalElements > 0) { + +
+ + + + + + + + + + + + + + + @for (audit of (auditsRD$ | async)?.payload?.page; track audit) { + + + + + + + + + + + } + +
{{ 'audit.overview.table.id' | translate }}{{ 'audit.overview.table.entityType' | translate }}{{ 'audit.overview.table.objectUUID' | translate }}{{ 'audit.overview.table.objectType' | translate }}{{ 'audit.overview.table.subjectUUID' | translate }}{{ 'audit.overview.table.subjectType' | translate }}{{ 'audit.overview.table.eperson' | translate }}{{ 'audit.overview.table.timestamp' | translate }}
{{audit.id}}{{ audit.eventType }}@if (audit.objectUUID) { + {{audit.objectUUID}} + }{{ audit.objectType }}{{ audit.subjectUUID }}{{ audit.subjectType }}{{ audit.epersonName }}{{ audit.timeStamp | date:dateFormat}}
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'audit.overview.table.id' | translate }}{{ 'audit.overview.table.entityType' | translate }}{{ 'audit.overview.table.objectUUID' | translate }}{{ 'audit.overview.table.objectType' | translate }}{{ 'audit.overview.table.subjectUUID' | translate }}{{ 'audit.overview.table.subjectType' | translate }}{{ 'audit.overview.table.eperson' | translate }}{{ 'audit.overview.table.timestamp' | translate }}
{{audit.id}}{{ audit.eventType }}{{audit.objectUUID}}{{ audit.objectType }}{{ audit.subjectUUID }}{{ audit.subjectType }}{{ePersonName}}{{ audit.timeStamp | date:dateFormat}}
-
-
- +
+ } + }
diff --git a/src/app/audit-page/overview/audit-overview.component.ts b/src/app/audit-page/overview/audit-overview.component.ts index b1994852944..dd90bf4e2e3 100644 --- a/src/app/audit-page/overview/audit-overview.component.ts +++ b/src/app/audit-page/overview/audit-overview.component.ts @@ -1,8 +1,6 @@ import { AsyncPipe, DatePipe, - NgForOf, - NgIf, } from '@angular/common'; import { Component, @@ -12,9 +10,14 @@ import { RouterLink } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { combineLatest, + forkJoin, Observable, } from 'rxjs'; -import { mergeMap } from 'rxjs/operators'; +import { + filter, + map, + mergeMap, +} from 'rxjs/operators'; import { AuditDataService } from '../../core/audit/audit-data.service'; import { Audit } from '../../core/audit/model/audit.model'; @@ -27,6 +30,7 @@ import { RemoteData } from '../../core/data/remote-data'; import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationComponent } from '../../shared/pagination/pagination.component'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { followLink } from '../../shared/utils/follow-link-config.model'; import { VarDirective } from '../../shared/utils/var.directive'; /** @@ -37,11 +41,9 @@ import { VarDirective } from '../../shared/utils/var.directive'; templateUrl: './audit-overview.component.html', imports: [ PaginationComponent, - NgIf, AsyncPipe, TranslateModule, RouterLink, - NgForOf, VarDirective, DatePipe, ], @@ -106,9 +108,29 @@ export class AuditOverviewComponent implements OnInit { this.auditsRD$ = combineLatest([this.isAdmin$, config$]).pipe( mergeMap(([isAdmin, config]) => { if (isAdmin) { - return this.auditService.findAll(config); + return this.auditService.findAll(config, true, true, followLink('eperson')); } }), + filter(data => data && data?.payload?.page?.length > 0), + mergeMap(auditsRD => { + const updatedAudits$ = auditsRD.payload.page.map(audit => { + return this.auditService.getEpersonName(audit).pipe( + map(name => Object.assign(new Audit(), audit, { epersonName: name })), + ); + }); + + return forkJoin(updatedAudits$).pipe( + map(updatedAudits => Object.assign(new RemoteData( + auditsRD.timeCompleted, + auditsRD.msToLive, + auditsRD.lastUpdated, + auditsRD.state, + auditsRD.errorMessage, + Object.assign(new PaginatedList(), { ...auditsRD.payload, page: updatedAudits }), + auditsRD.statusCode, + ))), + ); + }), ); } @@ -116,12 +138,4 @@ export class AuditOverviewComponent implements OnInit { return this.authorizationService.isAuthorized(FeatureID.AdministratorOf, undefined, undefined); } - /** - * Get the name of an EPerson by ID - * @param audit Audit object - */ - getEpersonName(audit: Audit): Observable { - return this.auditService.getEpersonName(audit); - } - } diff --git a/src/app/core/audit/audit-data.service.ts b/src/app/core/audit/audit-data.service.ts index 0a68b6e946a..f6b9026fc2c 100644 --- a/src/app/core/audit/audit-data.service.ts +++ b/src/app/core/audit/audit-data.service.ts @@ -30,6 +30,7 @@ import { PaginatedList } from '../data/paginated-list.model'; import { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; import { EPerson } from '../eperson/models/eperson.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { getFirstSucceededRemoteDataPayload, @@ -113,12 +114,10 @@ export class AuditDataService extends IdentifiableDataService{ * @param audit The audit object */ getEpersonName(audit: Audit): Observable { - // TODO: check why person is missing if (!audit.epersonUUID || !audit.eperson) { return of(AUDIT_PERSON_NOT_AVAILABLE); } - // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved return audit.eperson.pipe( getFirstSucceededRemoteDataWithNotEmptyPayload(), map((eperson: EPerson) => this.dsoNameService.getName(eperson)), @@ -130,13 +129,13 @@ export class AuditDataService extends IdentifiableDataService{ * @param audit * @param contextObjectId */ - getOtherObject(audit: Audit, contextObjectId: string): Observable { + getOtherObject(audit: Audit, contextObjectId: string): Observable { const otherObjectHref = this.getOtherObjectHref(audit, contextObjectId); if (otherObjectHref) { return this.findByHref(otherObjectHref).pipe( getFirstSucceededRemoteDataPayload(), - ); + ) as Observable; } return of(null); } diff --git a/src/app/core/audit/model/audit.model.ts b/src/app/core/audit/model/audit.model.ts index e476c3bd3e5..4789e1be56e 100644 --- a/src/app/core/audit/model/audit.model.ts +++ b/src/app/core/audit/model/audit.model.ts @@ -118,4 +118,14 @@ export class Audit implements CacheableObject { */ @link(ITEM) object?: Observable>; + + /** + * The name of the person who performed the action + */ + epersonName?: string; + + /** + * A different object connected to the current audited object + */ + otherAuditObject?: DSpaceObject; } diff --git a/src/app/shared/menu/providers/audit-item.menu.ts b/src/app/shared/menu/providers/audit-item.menu.ts index 5c9fefe7381..fedcfd59ed9 100644 --- a/src/app/shared/menu/providers/audit-item.menu.ts +++ b/src/app/shared/menu/providers/audit-item.menu.ts @@ -14,15 +14,15 @@ import { import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../../../core/data/remote-data'; +import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { LinkMenuItemModel } from '../menu-item/models/link.model'; import { MenuItemType } from '../menu-item-type.model'; import { PartialMenuSection } from '../menu-provider.model'; import { DSpaceObjectPageMenuProvider } from './helper-providers/dso.menu'; -import { FeatureID } from "../../../core/data/feature-authorization/feature-id"; -import { RemoteData } from "../../../core/data/remote-data"; -import { ConfigurationProperty } from "../../../core/shared/configuration-property.model"; /** * Menu provider to create the "Audit" option in the DSO audit menu @@ -43,8 +43,8 @@ export class AuditLogsMenuProvider extends DSpaceObjectPageMenuProvider { getFirstCompletedRemoteData(), map((response: RemoteData) => { return response.hasSucceeded ? (response.payload.values.length > 0 && response.payload.values[0] === 'true') : false; - }) - ) + }), + ), ]).pipe( map(([isAdmin, isAuditEnabled]: [boolean, boolean]) => { return [{ diff --git a/src/app/shared/menu/providers/audit-overview.menu.spec.ts b/src/app/shared/menu/providers/audit-overview.menu.spec.ts index 15d8c06bf6f..29aca63998a 100644 --- a/src/app/shared/menu/providers/audit-overview.menu.spec.ts +++ b/src/app/shared/menu/providers/audit-overview.menu.spec.ts @@ -13,8 +13,7 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { MenuItemType } from '../menu-item-type.model'; import { PartialMenuSection } from '../menu-provider.model'; -import { HealthMenuProvider } from './health.menu'; -import { AuditOverviewMenuProvider } from "./audit-overview.menu"; +import { AuditOverviewMenuProvider } from './audit-overview.menu'; describe('AuditOverviewMenuProvider', () => { const expectedSections: PartialMenuSection[] = [ diff --git a/src/app/shared/menu/providers/audit-overview.menu.ts b/src/app/shared/menu/providers/audit-overview.menu.ts index 4108cd7c40a..2d260098ebf 100644 --- a/src/app/shared/menu/providers/audit-overview.menu.ts +++ b/src/app/shared/menu/providers/audit-overview.menu.ts @@ -6,13 +6,20 @@ * http://www.dspace.org/license/ */ -import { Inject, Injectable } from '@angular/core'; +import { + Inject, + Injectable, +} from '@angular/core'; import { combineLatest, map, - Observable, of, + Observable, } from 'rxjs'; +import { + APP_CONFIG, + AppConfig, +} from '../../../../config/app-config.interface'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { MenuItemType } from '../menu-item-type.model'; @@ -20,7 +27,6 @@ import { AbstractMenuProvider, PartialMenuSection, } from '../menu-provider.model'; -import { APP_CONFIG, AppConfig } from "../../../../config/app-config.interface"; /** * Menu provider to create the "Health" menu in the admin sidebar diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index d269fb59b05..508a278837d 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -909,6 +909,8 @@ "audit.overview.table.eperson": "EPerson", + "audit.overview.table.other": "Other Object", + "audit.overview.table.timestamp": "Time", "audit.overview.breadcrumbs": "Audit Logs Overview", From 687edb17c8d7663488c6519a2752ad3f94c622f5 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Thu, 17 Jul 2025 10:21:12 +0200 Subject: [PATCH 04/22] [DURACOM-317] add missing breadcrumbs, fix tests, add missing labels --- src/app/audit-page/audit-page-routes.ts | 13 +-- .../object-audit-overview.component.html | 2 +- .../menu/providers/audit-item.menu.spec.ts | 37 ++++++++- .../providers/audit-overview.menu.spec.ts | 3 + src/app/shared/testing/audit.mock.ts | 81 +++++++++++++++++++ src/assets/i18n/en.json5 | 6 ++ 6 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 src/app/shared/testing/audit.mock.ts diff --git a/src/app/audit-page/audit-page-routes.ts b/src/app/audit-page/audit-page-routes.ts index 71e2dcbffb5..11c69e613d2 100644 --- a/src/app/audit-page/audit-page-routes.ts +++ b/src/app/audit-page/audit-page-routes.ts @@ -10,27 +10,30 @@ import { AuditOverviewComponent } from './overview/audit-overview.component'; export const ROUTES: Route[] = [ { path: '', - resolve: { breadcrumb: i18nBreadcrumbResolver }, - data: { breadcrumbKey: 'audit.overview' }, canActivate: [authenticatedGuard], children: [ { path: '', component: AuditOverviewComponent, - data: { title: 'audit.overview.title' }, + data: { title: 'audit.overview.title', breadcrumbKey: 'audit.overview' }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, }, { path: ':id', component: AuditDetailComponent, + data: { title: 'audit.detail.title', breadcrumbKey: 'audit.detail' }, resolve: { process: auditPageResolver, - // TODO: breadcrumbs resolver + breadcrumb: i18nBreadcrumbResolver, }, }, { path: 'object/:objectId', component: ObjectAuditOverviewComponent, - // TODO: breadcrumbs resolver + data: { title: 'audit.object.title', breadcrumbKey: 'audit.object' }, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, }, ], }, diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html index 3518f4d5141..cff0222cf94 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html @@ -57,7 +57,7 @@

{{ object.name }} ({{object.type}})

} - Back to Item + {{ 'audit.object.back' | translate }} } @if ((auditsRD$ | async)?.statusCode === 404) {

{{'audit.object.overview.disabled.message' | translate}}

diff --git a/src/app/shared/menu/providers/audit-item.menu.spec.ts b/src/app/shared/menu/providers/audit-item.menu.spec.ts index 9cda67bb157..8a9cab85810 100644 --- a/src/app/shared/menu/providers/audit-item.menu.spec.ts +++ b/src/app/shared/menu/providers/audit-item.menu.spec.ts @@ -1,7 +1,15 @@ +import { TestBed } from '@angular/core/testing'; +import { of as observableOf } from 'rxjs'; + +import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { Collection } from '../../../core/shared/collection.model'; import { COLLECTION } from '../../../core/shared/collection.resource-type'; +import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; import { URLCombiner } from '../../../core/url-combiner/url-combiner'; +import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; +import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { MenuItemType } from '../menu-item-type.model'; import { PartialMenuSection } from '../menu-provider.model'; import { AuditLogsMenuProvider } from './audit-item.menu'; @@ -14,7 +22,7 @@ describe('AuditLogsMenuProvider', () => { model: { type: MenuItemType.LINK, text: 'context-menu.actions.audit-item.btn', - link: new URLCombiner('/auditlogs/object').toString(), + link: new URLCombiner('/auditlogs/object/test-uuid').toString(), }, icon: 'key', }, @@ -28,6 +36,31 @@ describe('AuditLogsMenuProvider', () => { _links: { self: { href: 'self-link' } }, }); + const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'context-menu-entry.audit.enabled', + values: [ + 'true', + ], + })), + }); + + let authorizationServiceStub = new AuthorizationDataServiceStub(); + + beforeEach(() => { + spyOn(authorizationServiceStub, 'isAuthorized').and.returnValue( + observableOf(true), + ); + TestBed.configureTestingModule({ + providers: [ + AuditLogsMenuProvider, + { provide: AuthorizationDataService, useValue: authorizationServiceStub }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + ], + }); + provider = TestBed.inject(AuditLogsMenuProvider); + }); + it('should be created', () => { expect(provider).toBeTruthy(); }); @@ -40,6 +73,4 @@ describe('AuditLogsMenuProvider', () => { }); }); }); - - }); diff --git a/src/app/shared/menu/providers/audit-overview.menu.spec.ts b/src/app/shared/menu/providers/audit-overview.menu.spec.ts index 29aca63998a..1c46c0915f7 100644 --- a/src/app/shared/menu/providers/audit-overview.menu.spec.ts +++ b/src/app/shared/menu/providers/audit-overview.menu.spec.ts @@ -9,6 +9,8 @@ import { TestBed } from '@angular/core/testing'; import { of as observableOf } from 'rxjs'; +import { APP_CONFIG } from '../../../../config/app-config.interface'; +import { environment } from '../../../../environments/environment'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { MenuItemType } from '../menu-item-type.model'; @@ -40,6 +42,7 @@ describe('AuditOverviewMenuProvider', () => { providers: [ AuditOverviewMenuProvider, { provide: AuthorizationDataService, useValue: authorizationServiceStub }, + { provide: APP_CONFIG, useValue: environment }, ], }); provider = TestBed.inject(AuditOverviewMenuProvider); diff --git a/src/app/shared/testing/audit.mock.ts b/src/app/shared/testing/audit.mock.ts new file mode 100644 index 00000000000..746b7b5ce5d --- /dev/null +++ b/src/app/shared/testing/audit.mock.ts @@ -0,0 +1,81 @@ +import { Audit } from '../../core/audit/model/audit.model'; +import { EPerson } from '../../core/eperson/models/eperson.model'; + +export const AuditEPersonMock: EPerson = Object.assign(new EPerson(), { + handle: null, + groups: [], + netid: 'test@test.com', + lastActive: '2018-05-14T12:25:42.411+0000', + canLogIn: true, + email: 'test@test.com', + requireCertificate: false, + selfRegistered: false, + _links: { + self: { + href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons/4eebf0fa-cb9a-463e-8d4c-8a63122c7658', + }, + groups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons/4eebf0fa-cb9a-463e-8d4c-8a63122c7658/groups' }, + }, + id: '4eebf0fa-cb9a-463e-8d4c-8a63122c7658', + uuid: '4eebf0fa-cb9a-463e-8d4c-8a63122c7658', + type: 'eperson', + metadata: { + 'dc.title': [ + { + language: null, + value: 'User Test', + }, + ], + 'eperson.firstname': [ + { + language: null, + value: 'User', + }, + ], + 'eperson.lastname': [ + { + language: null, + value: 'Test', + }, + ], + 'eperson.language': [ + { + language: null, + value: 'en', + }, + ], + }, +}); + +export const AuditMock: Audit = Object.assign(new Audit(), { + detail: null, + epersonUUID: '4eebf0fa-cb9a-463e-8d4c-8a63122c7658', + eventType: 'MODIFY', + id: '6fcd7329-8439-4492-bb72-0a4240b52da8', + objectType: 'ITEM', + objectUUID: 'objectUUID', + subjectType: 'ITEM', + subjectUUID: '3a74fe2c-d353-4e33-9887-d50184662dd4', + timeStamp: '2020-11-13T10:41:06.223+0000', + type: 'auditevent', + _embedded: { + eperson: AuditEPersonMock, + }, + self: { + _links: { + eperson: { + href: 'https://dspace.4science.it/dspace-spring-rest/api/system/auditevents/6fcd7329-8439-4492-bb72-0a4240b52da8/eperson', + }, + object: { + href: 'https://dspace.4science.it/dspace-spring-rest/api/system/auditevents/6fcd7329-8439-4492-bb72-0a4240b52da8/object', + }, + self: { + href: 'https://dspace.4science.it/dspace-spring-rest/api/system/auditevents/6fcd7329-8439-4492-bb72-0a4240b52da8', + }, + subject: { + href: 'https://dspace.4science.it/dspace-spring-rest/api/system/auditevents/6fcd7329-8439-4492-bb72-0a4240b52da8/subject', + }, + }, + }, +}); + diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 508a278837d..c9c131d04cf 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -893,6 +893,8 @@ "admin.batch-import.page.remove": "remove", + "audit.detail.breadcrumbs": "Audit Details", + "audit.overview.title": "Audit Logs Overview", "audit.overview.table.id": "Audit ID", @@ -915,6 +917,10 @@ "audit.overview.breadcrumbs": "Audit Logs Overview", + "audit.object.back": "Back to Item", + + "audit.object.breadcrumbs": "Subject Audit Logs", + "audit.object.overview.title": "Subject Audit Logs Overview", "audit.object.overview.disabled.message": "Audit feature is currently disabled", From 54c396cd9adb83af22a90f412627373dff3903a0 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Thu, 17 Jul 2025 11:06:14 +0200 Subject: [PATCH 05/22] [DURACOM-317] fix accessibility issues, clean up, add e2e test for overview --- cypress/e2e/audit-overview-page.cy.ts | 16 ++++++++ src/app/audit-page/audit-page.resolver.ts | 3 +- .../detail/audit-detail.component.html | 16 ++++---- .../detail/audit-detail.component.ts | 32 +++++++++------- .../object-audit-overview.component.html | 10 ++--- .../object-audit-overview.component.ts | 2 - .../overview/audit-overview.component.html | 6 +-- .../menu/providers/audit-overview.menu.ts | 1 - src/assets/i18n/en.json5 | 38 +++++++++---------- 9 files changed, 70 insertions(+), 54 deletions(-) create mode 100644 cypress/e2e/audit-overview-page.cy.ts diff --git a/cypress/e2e/audit-overview-page.cy.ts b/cypress/e2e/audit-overview-page.cy.ts new file mode 100644 index 00000000000..ab0f2b41f96 --- /dev/null +++ b/cypress/e2e/audit-overview-page.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Audit Overview Page', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/auditlogs'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Page must first be visible + cy.get('ds-audit-overview').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-audit-overview'); + }); +}); diff --git a/src/app/audit-page/audit-page.resolver.ts b/src/app/audit-page/audit-page.resolver.ts index 8c98361744c..27b968088b3 100644 --- a/src/app/audit-page/audit-page.resolver.ts +++ b/src/app/audit-page/audit-page.resolver.ts @@ -10,6 +10,7 @@ import { AuditDataService } from '../core/audit/audit-data.service'; import { Audit } from '../core/audit/model/audit.model'; import { RemoteData } from '../core/data/remote-data'; import { getFirstSucceededRemoteData } from '../core/shared/operators'; +import { followLink } from '../shared/utils/follow-link-config.model'; /** @@ -24,7 +25,7 @@ export const auditPageResolver: ResolveFn> = ( state: RouterStateSnapshot, ): Observable> => { const auditService = inject(AuditDataService); - return auditService.findById(route.params.id).pipe( + return auditService.findById(route.params.id, true, true, followLink('eperson')).pipe( getFirstSucceededRemoteData(), ); }; diff --git a/src/app/audit-page/detail/audit-detail.component.html b/src/app/audit-page/detail/audit-detail.component.html index c6c0b5f950d..c5a22b3d7ac 100644 --- a/src/app/audit-page/detail/audit-detail.component.html +++ b/src/app/audit-page/detail/audit-detail.component.html @@ -1,25 +1,25 @@
-

{{'audit.detail.title' | translate }}

+

{{'audit.detail.title' | translate }}

@if (audit) { -

{{ 'audit.detail.id' | translate}}

+

{{ 'audit.detail.id' | translate}}

{{ audit.id }}
-

{{ 'audit.detail.eventType' | translate}}

+

{{ 'audit.detail.eventType' | translate}}

{{ audit.eventType }}
-

{{ 'audit.detail.subjectUUID' | translate}}

+

{{ 'audit.detail.subjectUUID' | translate}}

{{ audit.subjectUUID }}
-

{{ 'audit.detail.subjectType' | translate}}

+

{{ 'audit.detail.subjectType' | translate}}

{{ audit.subjectType }}
-

{{ 'audit.detail.eperson' | translate}}

-
{{ePersonName}}
+

{{ 'audit.detail.eperson' | translate}}

+
{{ audit.epersonName }}
-

{{ 'audit.detail.timeStamp' | translate}}

+

{{ 'audit.detail.timeStamp' | translate}}

{{ audit.timeStamp | date:dateFormat}}
} diff --git a/src/app/audit-page/detail/audit-detail.component.ts b/src/app/audit-page/detail/audit-detail.component.ts index b9e18747cf8..fdcccbd05ce 100644 --- a/src/app/audit-page/detail/audit-detail.component.ts +++ b/src/app/audit-page/detail/audit-detail.component.ts @@ -13,13 +13,15 @@ import { RouterLink, } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; -import { Observable } from 'rxjs'; +import { + Observable, + switchMap, +} from 'rxjs'; import { map } from 'rxjs/operators'; import { AuditDataService } from '../../core/audit/audit-data.service'; import { Audit } from '../../core/audit/model/audit.model'; import { AuthService } from '../../core/auth/auth.service'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { RemoteData } from '../../core/data/remote-data'; import { redirectOn4xx } from '../../core/shared/authorized.operators'; import { VarDirective } from '../../shared/utils/var.directive'; @@ -56,8 +58,7 @@ export class AuditDetailComponent implements OnInit { protected route: ActivatedRoute, protected router: Router, protected auditService: AuditDataService, - protected nameService: DSONameService) { - } + ) {} /** * Initialize component properties @@ -67,15 +68,20 @@ export class AuditDetailComponent implements OnInit { this.auditRD$ = this.route.data.pipe( map((data) => data.process as RemoteData), redirectOn4xx(this.router, this.authService), + switchMap((auditRD) => { + const epersonName$ = this.auditService.getEpersonName(auditRD.payload); + return epersonName$.pipe( + map( epersonName => new RemoteData( + auditRD.timeCompleted, + auditRD.msToLive, + auditRD.lastUpdated, + auditRD.state, + auditRD.errorMessage, + Object.assign(new Audit(), { ...auditRD.payload, epersonName }), + auditRD.statusCode, + )), + ); + }), ); } - - /** - * Get the name of an EPerson by ID - * @param audit Audit object - */ - getEpersonName(audit: Audit): Observable { - return this.auditService.getEpersonName(audit); - } - } diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html index cff0222cf94..2ccb659968a 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html @@ -1,15 +1,13 @@
-

{{'audit.object.overview.title' | translate}}

+

{{'audit.object.overview.title' | translate}}

@if (object) { -

{{ object.name }} ({{object.type}})

+

{{ object.name }} ({{object.type}})

@if ((auditsRD$ | async)?.payload; as audits) { @if (audits.totalElements === 0) { -
- No audits found. -
+
{{ 'audit.data.not-found' | translate }}
} @if (audits.totalElements > 0) { @@ -60,7 +58,7 @@

{{ object.name }} ({{object.type}})

{{ 'audit.object.back' | translate }} } @if ((auditsRD$ | async)?.statusCode === 404) { -

{{'audit.object.overview.disabled.message' | translate}}

+

{{'audit.object.overview.disabled.message' | translate}}

} } diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts index 2296756888d..a375e514dc4 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts @@ -186,8 +186,6 @@ export class ObjectAuditOverviewComponent implements OnInit { ); }), ); - - this.auditsRD$.subscribe(console.log); } isCurrentUserAdmin(): Observable { diff --git a/src/app/audit-page/overview/audit-overview.component.html b/src/app/audit-page/overview/audit-overview.component.html index f1014ce6b74..76c49cbaed9 100644 --- a/src/app/audit-page/overview/audit-overview.component.html +++ b/src/app/audit-page/overview/audit-overview.component.html @@ -1,13 +1,11 @@
-

{{'audit.overview.title' | translate}}

+

{{'audit.overview.title' | translate}}

@if (isAdmin$ | async) { @if ((auditsRD$ | async)?.payload?.totalElements === 0) { -
- No audits found. -
+
{{ 'audit.data.not-found' | translate }}
} @if ((auditsRD$ | async)?.payload?.totalElements > 0) { Date: Wed, 23 Jul 2025 17:40:28 +0200 Subject: [PATCH 06/22] [DURACOM-317] clean up, add expandable section, refactor to presentation component --- src/app/app-routes.ts | 2 +- src/app/audit-page/audit-page-routes.ts | 11 -- .../audit-page/audit-page.resolver.spec.ts | 37 ----- src/app/audit-page/audit-page.resolver.ts | 31 ---- .../audit-table/audit-table.component.html | 136 ++++++++++++++++++ .../audit-table/audit-table.component.ts | 79 ++++++++++ .../detail/audit-detail.component.html | 33 ----- .../detail/audit-detail.component.ts | 87 ----------- .../object-audit-overview.component.html | 56 +------- .../object-audit-overview.component.ts | 28 ++-- .../overview/audit-overview.component.html | 52 ++----- .../overview/audit-overview.component.ts | 29 ++-- src/app/core/audit/audit-data.service.ts | 36 ++--- src/app/core/audit/model/audit.model.ts | 54 +++++++ src/app/shared/utils/string-replace.pipe.ts | 18 +++ src/app/shared/utils/string-replace.spec.ts | 38 +++++ src/assets/i18n/en.json5 | 14 ++ 17 files changed, 405 insertions(+), 336 deletions(-) delete mode 100644 src/app/audit-page/audit-page.resolver.spec.ts delete mode 100644 src/app/audit-page/audit-page.resolver.ts create mode 100644 src/app/audit-page/audit-table/audit-table.component.html create mode 100644 src/app/audit-page/audit-table/audit-table.component.ts delete mode 100644 src/app/audit-page/detail/audit-detail.component.html delete mode 100644 src/app/audit-page/detail/audit-detail.component.ts create mode 100644 src/app/shared/utils/string-replace.pipe.ts create mode 100644 src/app/shared/utils/string-replace.spec.ts diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index 297ced23a1c..ba1c206dfc5 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -268,7 +268,7 @@ export const APP_ROUTES: Route[] = [ { path: 'auditlogs', loadChildren: () => import('./audit-page/audit-page-routes').then((m) => m.ROUTES), - canActivate: [authenticatedGuard, endUserAgreementCurrentUserGuard], + canActivate: [siteAdministratorGuard, endUserAgreementCurrentUserGuard], }, { path: 'subscriptions', diff --git a/src/app/audit-page/audit-page-routes.ts b/src/app/audit-page/audit-page-routes.ts index 11c69e613d2..4f5a85f6b99 100644 --- a/src/app/audit-page/audit-page-routes.ts +++ b/src/app/audit-page/audit-page-routes.ts @@ -2,8 +2,6 @@ import { Route } from '@angular/router'; import { authenticatedGuard } from '../core/auth/authenticated.guard'; import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { auditPageResolver } from './audit-page.resolver'; -import { AuditDetailComponent } from './detail/audit-detail.component'; import { ObjectAuditOverviewComponent } from './object-audit-overview/object-audit-overview.component'; import { AuditOverviewComponent } from './overview/audit-overview.component'; @@ -18,15 +16,6 @@ export const ROUTES: Route[] = [ data: { title: 'audit.overview.title', breadcrumbKey: 'audit.overview' }, resolve: { breadcrumb: i18nBreadcrumbResolver }, }, - { - path: ':id', - component: AuditDetailComponent, - data: { title: 'audit.detail.title', breadcrumbKey: 'audit.detail' }, - resolve: { - process: auditPageResolver, - breadcrumb: i18nBreadcrumbResolver, - }, - }, { path: 'object/:objectId', component: ObjectAuditOverviewComponent, diff --git a/src/app/audit-page/audit-page.resolver.spec.ts b/src/app/audit-page/audit-page.resolver.spec.ts deleted file mode 100644 index a7b10dff3ec..00000000000 --- a/src/app/audit-page/audit-page.resolver.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; - -import { AuditDataService } from '../core/audit/audit-data.service'; -import { Audit } from '../core/audit/model/audit.model'; -import { RemoteData } from '../core/data/remote-data'; -import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { auditPageResolver } from './audit-page.resolver'; - -describe('auditPageResolver', () => { - let auditService: any; - - beforeEach(() => { - auditService = { - findById: jasmine.createSpy('findById').and.callFake((id: string) => createSuccessfulRemoteDataObject$({ id })), - }; - - TestBed.configureTestingModule({ - providers: [ - { provide: AuditDataService, useValue: auditService }, - ], - }); - }); - - it('should resolve an audit with the correct id', (done) => { - const uuid = '1234-65487-12354-1235'; - const obs = TestBed.runInInjectionContext(() => { - return auditPageResolver({ params: { id: uuid } } as any, undefined); - }) as Observable>; - - obs.pipe(first()).subscribe((resolved) => { - expect(resolved.payload.id).toEqual(uuid); - done(); - }); - }); -}); diff --git a/src/app/audit-page/audit-page.resolver.ts b/src/app/audit-page/audit-page.resolver.ts deleted file mode 100644 index 27b968088b3..00000000000 --- a/src/app/audit-page/audit-page.resolver.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { inject } from '@angular/core'; -import { - ActivatedRouteSnapshot, - ResolveFn, - RouterStateSnapshot, -} from '@angular/router'; -import { Observable } from 'rxjs'; - -import { AuditDataService } from '../core/audit/audit-data.service'; -import { Audit } from '../core/audit/model/audit.model'; -import { RemoteData } from '../core/data/remote-data'; -import { getFirstSucceededRemoteData } from '../core/shared/operators'; -import { followLink } from '../shared/utils/follow-link-config.model'; - - -/** - * Method for resolving an audit based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found process based on the parameters in the current route, - * or an error if something went wrong - */ -export const auditPageResolver: ResolveFn> = ( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot, -): Observable> => { - const auditService = inject(AuditDataService); - return auditService.findById(route.params.id, true, true, followLink('eperson')).pipe( - getFirstSucceededRemoteData(), - ); -}; diff --git a/src/app/audit-page/audit-table/audit-table.component.html b/src/app/audit-page/audit-table/audit-table.component.html new file mode 100644 index 00000000000..df07c0fbb46 --- /dev/null +++ b/src/app/audit-page/audit-table/audit-table.component.html @@ -0,0 +1,136 @@ +@if (audits.totalElements === 0) { +
{{ 'audit.data.not-found' | translate }}
+} @else { + +
+ + + + + + + @if (isOverviewPage) { + + + + + } @else { + + } + + + + @for (audit of audits?.page; track audit) { + + + + + @if (isOverviewPage) { + + + + + } @else { + + } + + @if (audit.hasDetails) { + + + + } + } + +
{{ 'audit.overview.table.entityType' | translate }}{{ 'audit.overview.table.eperson' | translate }}{{ 'audit.overview.table.timestamp' | translate }}{{ 'audit.overview.table.objectUUID' | translate }}{{ 'audit.overview.table.objectType' | translate }}{{ 'audit.overview.table.subjectUUID' | translate }}{{ 'audit.overview.table.subjectType' | translate }}{{ 'audit.overview.table.other' | translate }}
+ @if (audit.hasDetails) { +
+ +
+ {{ audit.eventType }} +
+
+ } @else { +
+ {{ audit.eventType }} +
+ } +
{{ audit.epersonName }}{{ audit.timeStamp | date:dateFormat}} + @if (audit.objectUUID) { + {{audit.objectUUID}} + } + {{ audit.objectType }}{{ audit.subjectUUID }}{{ audit.subjectType }} + @if (object && object.id === audit.objectUUID) { + + @if (audit.otherAuditObject; as dso) { + {{ dsoNameService.getName(dso) }} ({{ dso.type }}) + } @else { + {{ dataNotAvailable }} + } + + } @else { + {{ dataNotAvailable }} + } +
+
+
+ @if (audit.metadataField) { +
+ {{"audit.detail.metadata.field" | translate}} + {{ audit.metadataField | dsStringReplace: "_":"." }} +
+ } + @if (audit.value) { +
+ {{"audit.detail.metadata.value" | translate}} + + {{ audit.value }} + +
+ } + @if (audit.authority) { +
+ {{"audit.detail.metadata.authority" | translate}} + {{ audit.authority }} +
+ } + @if (audit.confidence !== null) { +
+ {{"audit.detail.metadata.confidence" | translate}} + {{ audit.confidence }} +
+ } + @if (audit.place !== null) { +
+ {{"audit.detail.metadata.place" | translate}} + {{ audit.place }} +
+ } + @if (audit.action) { +
+ {{"audit.detail.metadata.action" | translate}} + {{ audit.action }} +
+ } + @if (audit.checksum) { +
+ {{"audit.detail.metadata.checksum" | translate}} + {{ audit.checksum }} +
+ } +
+
+
+
+
+} + diff --git a/src/app/audit-page/audit-table/audit-table.component.ts b/src/app/audit-page/audit-table/audit-table.component.ts new file mode 100644 index 00000000000..14daa1feeb6 --- /dev/null +++ b/src/app/audit-page/audit-table/audit-table.component.ts @@ -0,0 +1,79 @@ +import { + AsyncPipe, + DatePipe, + NgClass, +} from '@angular/common'; +import { + ChangeDetectorRef, + Component, + Input, +} from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; + +import { AUDIT_PERSON_NOT_AVAILABLE } from '../../core/audit/audit-data.service'; +import { Audit } from '../../core/audit/model/audit.model'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { PaginatedList } from '../../core/data/paginated-list.model'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { PaginationComponent } from '../../shared/pagination/pagination.component'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { StringReplacePipe } from '../../shared/utils/string-replace.pipe'; +import { VarDirective } from '../../shared/utils/var.directive'; + +@Component({ + selector: 'ds-audit-table', + templateUrl: './audit-table.component.html', + imports: [ + PaginationComponent, + AsyncPipe, + TranslateModule, + VarDirective, + RouterLink, + DatePipe, + StringReplacePipe, + NgClass, + NgbCollapseModule, + ], + standalone: true, +}) +export class AuditTableComponent { + /** + * The Audit items to be shown + */ + @Input() audits: PaginatedList; + + /** + * Config for pagination + */ + @Input() pageConfig: PaginationComponentOptions; + + /** + * Whether the table is used for a an overview of all the site's Audits + */ + @Input() isOverviewPage: boolean; + + /** + * The DSpaceObject used in case of a detail audit page + */ + @Input() object: DSpaceObject; + + protected readonly dataNotAvailable = AUDIT_PERSON_NOT_AVAILABLE; + + /** + * Date format to use for start and end time of audits + */ + protected readonly dateFormat = 'yyyy-MM-dd HH:mm:ss'; + + constructor( + public dsoNameService: DSONameService, + private changeDetectorRef: ChangeDetectorRef, + ) {} + + + toggleCollapse(audit: Audit) { + audit.isCollapsed = !audit.isCollapsed; + this.changeDetectorRef.detectChanges(); + } +} diff --git a/src/app/audit-page/detail/audit-detail.component.html b/src/app/audit-page/detail/audit-detail.component.html deleted file mode 100644 index c5a22b3d7ac..00000000000 --- a/src/app/audit-page/detail/audit-detail.component.html +++ /dev/null @@ -1,33 +0,0 @@ -
-
-

{{'audit.detail.title' | translate }}

-
- - @if (audit) { -

{{ 'audit.detail.id' | translate}}

-
{{ audit.id }}
- -

{{ 'audit.detail.eventType' | translate}}

-
{{ audit.eventType }}
- -

{{ 'audit.detail.subjectUUID' | translate}}

-
{{ audit.subjectUUID }}
- -

{{ 'audit.detail.subjectType' | translate}}

-
{{ audit.subjectType }}
- -

{{ 'audit.detail.eperson' | translate}}

-
{{ audit.epersonName }}
- -

{{ 'audit.detail.timeStamp' | translate}}

-
{{ audit.timeStamp | date:dateFormat}}
- } - - - {{'audit.detail.back' | translate}} - - @if (audit.objectUUID) { - {{'audit.detail.back.subject' | translate}} - } - -
diff --git a/src/app/audit-page/detail/audit-detail.component.ts b/src/app/audit-page/detail/audit-detail.component.ts deleted file mode 100644 index fdcccbd05ce..00000000000 --- a/src/app/audit-page/detail/audit-detail.component.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { - AsyncPipe, - DatePipe, -} from '@angular/common'; -import { - ChangeDetectionStrategy, - Component, - OnInit, -} from '@angular/core'; -import { - ActivatedRoute, - Router, - RouterLink, -} from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; -import { - Observable, - switchMap, -} from 'rxjs'; -import { map } from 'rxjs/operators'; - -import { AuditDataService } from '../../core/audit/audit-data.service'; -import { Audit } from '../../core/audit/model/audit.model'; -import { AuthService } from '../../core/auth/auth.service'; -import { RemoteData } from '../../core/data/remote-data'; -import { redirectOn4xx } from '../../core/shared/authorized.operators'; -import { VarDirective } from '../../shared/utils/var.directive'; - -/** - * A component displaying detailed information about a DSpace Audit - */ -@Component({ - selector: 'ds-audit-detail', - templateUrl: './audit-detail.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - AsyncPipe, - TranslateModule, - VarDirective, - DatePipe, - RouterLink, - ], - standalone: true, -}) -export class AuditDetailComponent implements OnInit { - - /** - * The Audit's Remote Data - */ - auditRD$: Observable>; - - /** - * Date format to use for start and end time of audits - */ - dateFormat = 'yyyy-MM-dd HH:mm:ss'; - - constructor(protected authService: AuthService, - protected route: ActivatedRoute, - protected router: Router, - protected auditService: AuditDataService, - ) {} - - /** - * Initialize component properties - * Display a 404 if the audit doesn't exist - */ - ngOnInit(): void { - this.auditRD$ = this.route.data.pipe( - map((data) => data.process as RemoteData), - redirectOn4xx(this.router, this.authService), - switchMap((auditRD) => { - const epersonName$ = this.auditService.getEpersonName(auditRD.payload); - return epersonName$.pipe( - map( epersonName => new RemoteData( - auditRD.timeCompleted, - auditRD.msToLive, - auditRD.lastUpdated, - auditRD.state, - auditRD.errorMessage, - Object.assign(new Audit(), { ...auditRD.payload, epersonName }), - auditRD.statusCode, - )), - ); - }), - ); - } -} diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html index 2ccb659968a..49c1aeb7e62 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html @@ -6,57 +6,13 @@

{{'audit.object.overview.title' | translate}}

@if (object) {

{{ object.name }} ({{object.type}})

@if ((auditsRD$ | async)?.payload; as audits) { - @if (audits.totalElements === 0) { -
{{ 'audit.data.not-found' | translate }}
- } - - @if (audits.totalElements > 0) { - - -
- - - - - - - - - - - - @for (audit of audits.page; track audit) { - - - - - - - - } - -
{{ 'audit.overview.table.id' | translate }}{{ 'audit.overview.table.entityType' | translate }}{{ 'audit.overview.table.eperson' | translate }}{{ 'audit.overview.table.timestamp' | translate }}{{ 'audit.overview.table.other' | translate }}
{{audit.id}}{{ audit.eventType }}{{ audit.epersonName }}{{ audit.timeStamp | date:dateFormat}} - @if (object.id === audit.objectUUID) { - - @if (audit.otherAuditObject; as dso) { - {{ dsoNameService.getName(dso) }} ({{ dso.type }}) - } @else { - {{ dataNotAvailable }} - } - - } @else { - {{ dataNotAvailable }} - } -
-
-
- } - {{ 'audit.object.back' | translate }} + } + {{ 'audit.object.back' | translate }} @if ((auditsRD$ | async)?.statusCode === 404) {

{{'audit.object.overview.disabled.message' | translate}}

} diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts index a375e514dc4..1e2f1e84149 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts @@ -17,7 +17,6 @@ import { combineLatest, forkJoin, Observable, - of, } from 'rxjs'; import { filter, @@ -51,7 +50,7 @@ import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { PaginationComponent } from '../../shared/pagination/pagination.component'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { VarDirective } from '../../shared/utils/var.directive'; - +import { AuditTableComponent } from '../audit-table/audit-table.component'; /** * Component displaying a list of all audit about a object in a paginated table */ @@ -65,6 +64,7 @@ import { VarDirective } from '../../shared/utils/var.directive'; VarDirective, RouterLink, DatePipe, + AuditTableComponent, ], standalone: true, }) @@ -144,23 +144,26 @@ export class ObjectAuditOverviewComponent implements OnInit { */ setAudits() { const config$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config); - const isAdmin$ = this.isCurrentUserAdmin(); const parentCommunity$ = this.owningCollection$.pipe( switchMap(collection => collection.parentCommunity), getFirstCompletedRemoteData(), map(data => data?.payload), ); - this.auditsRD$ = combineLatest([isAdmin$, config$, this.owningCollection$, parentCommunity$]).pipe( - mergeMap(([isAdmin, config, owningCollection, parentCommunity]) => { - if (isAdmin) { - return this.auditService.findByObject(this.object.id, config, owningCollection.id, parentCommunity.id).pipe( - getFirstCompletedRemoteData(), - ); - } - return of(null); - }), + this.auditsRD$ = combineLatest([ config$, this.owningCollection$, parentCommunity$]).pipe( + switchMap(([config, owningCollection, parentCommunity]) => + this.auditService.findByObject(this.object.id, config, owningCollection.id, parentCommunity.id).pipe( + getFirstCompletedRemoteData(), + ), + ), filter(data => data && data?.payload?.page?.length > 0), + map((audits) => { + audits.payload?.page.forEach((audit) => { + audit.hasDetails = this.auditService.auditHasDetails(audit); + }); + + return audits; + }), mergeMap(auditsRD => { const updatedAudits$ = auditsRD.payload.page.map(audit => { return forkJoin({ @@ -172,7 +175,6 @@ export class ObjectAuditOverviewComponent implements OnInit { ), ); }); - return forkJoin(updatedAudits$).pipe( map(updatedAudits => Object.assign(new RemoteData( auditsRD.timeCompleted, diff --git a/src/app/audit-page/overview/audit-overview.component.html b/src/app/audit-page/overview/audit-overview.component.html index 76c49cbaed9..c7ba6b70b3b 100644 --- a/src/app/audit-page/overview/audit-overview.component.html +++ b/src/app/audit-page/overview/audit-overview.component.html @@ -3,50 +3,14 @@

{{'audit.overview.title' | translate}}

- @if (isAdmin$ | async) { - @if ((auditsRD$ | async)?.payload?.totalElements === 0) { -
{{ 'audit.data.not-found' | translate }}
- } - @if ((auditsRD$ | async)?.payload?.totalElements > 0) { - -
- - - - - - - - - - - - - - - @for (audit of (auditsRD$ | async)?.payload?.page; track audit) { - - - - - - - - - - - } - -
{{ 'audit.overview.table.id' | translate }}{{ 'audit.overview.table.entityType' | translate }}{{ 'audit.overview.table.objectUUID' | translate }}{{ 'audit.overview.table.objectType' | translate }}{{ 'audit.overview.table.subjectUUID' | translate }}{{ 'audit.overview.table.subjectType' | translate }}{{ 'audit.overview.table.eperson' | translate }}{{ 'audit.overview.table.timestamp' | translate }}
{{audit.id}}{{ audit.eventType }}@if (audit.objectUUID) { - {{audit.objectUUID}} - }{{ audit.objectType }}{{ audit.subjectUUID }}{{ audit.subjectType }}{{ audit.epersonName }}{{ audit.timeStamp | date:dateFormat}}
-
-
- } + @if ((auditsRD$ | async)?.payload; as audits) { + + } @else { +
{{ 'audit.data.not-found' | translate }}
}
diff --git a/src/app/audit-page/overview/audit-overview.component.ts b/src/app/audit-page/overview/audit-overview.component.ts index dd90bf4e2e3..3ade6dcbb7d 100644 --- a/src/app/audit-page/overview/audit-overview.component.ts +++ b/src/app/audit-page/overview/audit-overview.component.ts @@ -1,6 +1,7 @@ import { AsyncPipe, DatePipe, + JsonPipe, } from '@angular/common'; import { Component, @@ -9,9 +10,9 @@ import { import { RouterLink } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { - combineLatest, forkJoin, Observable, + switchMap, } from 'rxjs'; import { filter, @@ -32,6 +33,7 @@ import { PaginationComponent } from '../../shared/pagination/pagination.componen import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { followLink } from '../../shared/utils/follow-link-config.model'; import { VarDirective } from '../../shared/utils/var.directive'; +import { AuditTableComponent } from '../audit-table/audit-table.component'; /** * Component displaying a list of all audit in a paginated table @@ -46,6 +48,8 @@ import { VarDirective } from '../../shared/utils/var.directive'; RouterLink, VarDirective, DatePipe, + JsonPipe, + AuditTableComponent, ], standalone: true, }) @@ -56,11 +60,6 @@ export class AuditOverviewComponent implements OnInit { */ auditsRD$: Observable>>; - /** - * Whether user is admin - */ - isAdmin$: Observable; - /** * The current pagination configuration for the page used by the FindAll method */ @@ -103,15 +102,18 @@ export class AuditOverviewComponent implements OnInit { * Send a request to fetch all audits for the current page */ setAudits() { - const config$ = this.paginationService.getFindListOptions(this.pageId, this.config); - this.isAdmin$ = this.isCurrentUserAdmin(); - this.auditsRD$ = combineLatest([this.isAdmin$, config$]).pipe( - mergeMap(([isAdmin, config]) => { - if (isAdmin) { - return this.auditService.findAll(config, true, true, followLink('eperson')); - } + this.auditsRD$ = this.paginationService.getFindListOptions(this.pageId, this.config).pipe( + switchMap((config) => { + return this.auditService.findAll(config, true, true, followLink('eperson')); }), filter(data => data && data?.payload?.page?.length > 0), + map((audits) => { + audits.payload?.page.forEach((audit) => { + audit.hasDetails = this.auditService.auditHasDetails(audit); + }); + + return audits; + }), mergeMap(auditsRD => { const updatedAudits$ = auditsRD.payload.page.map(audit => { return this.auditService.getEpersonName(audit).pipe( @@ -134,6 +136,7 @@ export class AuditOverviewComponent implements OnInit { ); } + isCurrentUserAdmin(): Observable { return this.authorizationService.isAuthorized(FeatureID.AdministratorOf, undefined, undefined); } diff --git a/src/app/core/audit/audit-data.service.ts b/src/app/core/audit/audit-data.service.ts index f6b9026fc2c..c2a88b50023 100644 --- a/src/app/core/audit/audit-data.service.ts +++ b/src/app/core/audit/audit-data.service.ts @@ -3,10 +3,7 @@ import { Observable, of, } from 'rxjs'; -import { - map, - startWith, -} from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { hasValue } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -30,12 +27,8 @@ import { PaginatedList } from '../data/paginated-list.model'; import { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; import { EPerson } from '../eperson/models/eperson.model'; -import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { - getFirstSucceededRemoteDataPayload, - getFirstSucceededRemoteDataWithNotEmptyPayload, -} from '../shared/operators'; +import { getFirstCompletedRemoteData } from '../shared/operators'; import { Audit } from './model/audit.model'; export const AUDIT_PERSON_NOT_AVAILABLE = 'n/a'; @@ -114,14 +107,14 @@ export class AuditDataService extends IdentifiableDataService{ * @param audit The audit object */ getEpersonName(audit: Audit): Observable { - if (!audit.epersonUUID || !audit.eperson) { + if (!audit.eperson) { return of(AUDIT_PERSON_NOT_AVAILABLE); } return audit.eperson.pipe( - getFirstSucceededRemoteDataWithNotEmptyPayload(), - map((eperson: EPerson) => this.dsoNameService.getName(eperson)), - startWith(AUDIT_PERSON_NOT_AVAILABLE)); + getFirstCompletedRemoteData(), + map((epersonRd: RemoteData) => epersonRd.payload ? this.dsoNameService.getName(epersonRd.payload) : AUDIT_PERSON_NOT_AVAILABLE), + ); } /** @@ -129,13 +122,14 @@ export class AuditDataService extends IdentifiableDataService{ * @param audit * @param contextObjectId */ - getOtherObject(audit: Audit, contextObjectId: string): Observable { + getOtherObject(audit: Audit, contextObjectId: string): Observable { const otherObjectHref = this.getOtherObjectHref(audit, contextObjectId); if (otherObjectHref) { return this.findByHref(otherObjectHref).pipe( - getFirstSucceededRemoteDataPayload(), - ) as Observable; + getFirstCompletedRemoteData(), + map(rd => rd.payload ?? null), + ); } return of(null); } @@ -155,4 +149,14 @@ export class AuditDataService extends IdentifiableDataService{ } } + auditHasDetails(audit: Audit): boolean { + return hasValue(audit.metadataField) + || hasValue(audit.authority) + || hasValue(audit.confidence) + || hasValue(audit.checksum) + || hasValue(audit.authority) + || hasValue(audit.place) + || hasValue(audit.value); + } + } diff --git a/src/app/core/audit/model/audit.model.ts b/src/app/core/audit/model/audit.model.ts index 4789e1be56e..9fa1c6a9e9a 100644 --- a/src/app/core/audit/model/audit.model.ts +++ b/src/app/core/audit/model/audit.model.ts @@ -87,6 +87,60 @@ export class Audit implements CacheableObject { @autoserialize timeStamp: string; + /** + * The audited metadata + */ + @autoserialize + metadataField: string; + + /** + * The audited value + */ + @autoserialize + value: string; + + /** + * The related authority + */ + @autoserialize + authority: string; + + /** + * The confidence of the audit + */ + @autoserialize + confidence: number; + + /** + * The place of the audit + */ + @autoserialize + place: number; + + /** + * The action type of the audit + */ + @autoserialize + action: string; + + /** + * The checksum of the audit + */ + @autoserialize + checksum: string; + + /** + * Property to expand details section + */ + @autoserialize + isCollapsed = true; + + /** + * Property to check if audit has details + */ + @autoserialize + hasDetails: boolean; + /** * The {@link HALLink}s for this Audit */ diff --git a/src/app/shared/utils/string-replace.pipe.ts b/src/app/shared/utils/string-replace.pipe.ts new file mode 100644 index 00000000000..2e7eeb6a257 --- /dev/null +++ b/src/app/shared/utils/string-replace.pipe.ts @@ -0,0 +1,18 @@ +import { + Pipe, + PipeTransform, +} from '@angular/core'; + +import { hasValue } from '../empty.util'; + +@Pipe({ + name: 'dsStringReplace', + standalone: true, +}) +export class StringReplacePipe implements PipeTransform { + + transform(value: string, regexValue: string, replaceValue: string): string { + const regex = new RegExp(regexValue, 'g'); + return hasValue(value) ? value.replace(regex, replaceValue) : value; + } +} diff --git a/src/app/shared/utils/string-replace.spec.ts b/src/app/shared/utils/string-replace.spec.ts new file mode 100644 index 00000000000..f6f6e20cc44 --- /dev/null +++ b/src/app/shared/utils/string-replace.spec.ts @@ -0,0 +1,38 @@ +import { TestBed } from '@angular/core/testing'; + +import { StringReplacePipe } from './string-replace.pipe'; + +describe('StringReplacePipe Pipe', () => { + + let stringReplacePipe: StringReplacePipe; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + StringReplacePipe, + ], + }).compileComponents(); + + stringReplacePipe = TestBed.inject(StringReplacePipe); + }); + + it('should replace the character specified in the regex parameter', async () => { + testTransform( + 'This_is_a_test', '_', ' ', 'This is a test', + ); + }); + + it('should not transform empty value', () => { + testTransform( + '', '_', ' ', '', + ); + }); + + function testTransform(input: string, regex: string, replaceValue: string, output: string) { + expect( + stringReplacePipe.transform(input, regex, replaceValue), + ).toMatch( + output, + ); + } +}); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index dfb2c1dc2de..6a97e257a41 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -914,6 +914,20 @@ "audit.detail.back.subject": "Subject Audit Logs", + "audit.detail.metadata.field": "Metadata field:", + + "audit.detail.metadata.value": "Value:", + + "audit.detail.metadata.authority": "Authority:", + + "audit.detail.metadata.confidence": "Confidence:", + + "audit.detail.metadata.place": "Place:", + + "audit.detail.metadata.action": "Action:", + + "audit.detail.metadata.checksum": "Checksum:", + "audit.overview.title": "Audit Logs Overview", "audit.overview.table.id": "Audit ID", From 6c2c2d6c622af7bf57f4ee0f4e64cd2ef692def7 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Thu, 24 Jul 2025 13:25:12 +0200 Subject: [PATCH 07/22] [DURACOM-317] clean up, add tests --- .../audit-table/audit-table.component.html | 109 ++++++++------- .../audit-table/audit-table.component.spec.ts | 97 +++++++++++++ .../audit-table/audit-table.component.ts | 2 + .../object-audit-overview.component.spec.ts | 111 +++++++++++++++ .../object-audit-overview.component.ts | 53 +++---- .../overview/audit-overview.component.spec.ts | 131 ++++-------------- .../overview/audit-overview.component.ts | 22 +-- src/app/shared/testing/audit.mock.ts | 1 + 8 files changed, 314 insertions(+), 212 deletions(-) create mode 100644 src/app/audit-page/audit-table/audit-table.component.spec.ts create mode 100644 src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts diff --git a/src/app/audit-page/audit-table/audit-table.component.html b/src/app/audit-page/audit-table/audit-table.component.html index df07c0fbb46..bc673920269 100644 --- a/src/app/audit-page/audit-table/audit-table.component.html +++ b/src/app/audit-page/audit-table/audit-table.component.html @@ -75,56 +75,15 @@ @if (audit.hasDetails) { - -
-
- @if (audit.metadataField) { -
- {{"audit.detail.metadata.field" | translate}} - {{ audit.metadataField | dsStringReplace: "_":"." }} -
- } - @if (audit.value) { -
- {{"audit.detail.metadata.value" | translate}} - - {{ audit.value }} - -
- } - @if (audit.authority) { -
- {{"audit.detail.metadata.authority" | translate}} - {{ audit.authority }} -
- } - @if (audit.confidence !== null) { -
- {{"audit.detail.metadata.confidence" | translate}} - {{ audit.confidence }} -
- } - @if (audit.place !== null) { -
- {{"audit.detail.metadata.place" | translate}} - {{ audit.place }} -
- } - @if (audit.action) { -
- {{"audit.detail.metadata.action" | translate}} - {{ audit.action }} -
- } - @if (audit.checksum) { -
- {{"audit.detail.metadata.checksum" | translate}} - {{ audit.checksum }} -
- } -
-
- + @if (isOverviewPage) { + + + + } @else { + + + + } } } @@ -134,3 +93,53 @@ } + +
+
+ @if (audit.metadataField) { +
+ {{"audit.detail.metadata.field" | translate}} + {{ audit.metadataField | dsStringReplace: "_":"." }} +
+ } + @if (audit.value) { +
+ {{"audit.detail.metadata.value" | translate}} + + {{ audit.value }} + +
+ } + @if (audit.authority) { +
+ {{"audit.detail.metadata.authority" | translate}} + {{ audit.authority }} +
+ } + @if (audit.confidence !== null) { +
+ {{"audit.detail.metadata.confidence" | translate}} + {{ audit.confidence }} +
+ } + @if (audit.place !== null) { +
+ {{"audit.detail.metadata.place" | translate}} + {{ audit.place }} +
+ } + @if (audit.action) { +
+ {{"audit.detail.metadata.action" | translate}} + {{ audit.action }} +
+ } + @if (audit.checksum) { +
+ {{"audit.detail.metadata.checksum" | translate}} + {{ audit.checksum }} +
+ } +
+
+
diff --git a/src/app/audit-page/audit-table/audit-table.component.spec.ts b/src/app/audit-page/audit-table/audit-table.component.spec.ts new file mode 100644 index 00000000000..5191d56091e --- /dev/null +++ b/src/app/audit-page/audit-table/audit-table.component.spec.ts @@ -0,0 +1,97 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { PaginatedList } from 'src/app/core/data/paginated-list.model'; + +import { Audit } from '../../core/audit/model/audit.model'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../shared/mocks/dso-name.service.mock'; +import { PaginationComponent } from '../../shared/pagination/pagination.component'; +import { AuditMock } from '../../shared/testing/audit.mock'; +import { AuditTableComponent } from './audit-table.component'; + +describe('AuditTableComponent', () => { + let component: AuditTableComponent; + let fixture: ComponentFixture; + + let audits = new PaginatedList() as PaginatedList; + + beforeEach(waitForAsync(() => { + audits.page = [ AuditMock ]; + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + RouterTestingModule.withRoutes([]), + AuditTableComponent, + PaginationComponent, + ], + providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(AuditTableComponent, { + remove: { imports: [PaginationComponent] }, + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AuditTableComponent); + component = fixture.componentInstance; + component.audits = audits; + component.isOverviewPage = true; + fixture.detectChanges(); + }); + + describe('table structure', () => { + + it('should display the entityType in the first column', () => { + const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); + const el = rowElements[0].query(By.css('td:nth-child(1)')).nativeElement; + expect(el.textContent).toContain(audits.page[0].eventType); + }); + + it('should display the eperson in the second column', () => { + const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); + const el = rowElements[0].query(By.css('td:nth-child(2)')).nativeElement; + expect(el.textContent).toContain(audits.page[0].epersonName); + }); + + it('should display the timestamp in the third column', () => { + const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); + const el = rowElements[0].query(By.css('td:nth-child(3)')).nativeElement; + expect(el.textContent).toContain('2020-11-13 11:41:06'); + }); + + it('should display the objectUUID in the fourth column', () => { + const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); + const el = rowElements[0].query(By.css('td:nth-child(4)')).nativeElement; + expect(el.textContent).toContain(audits.page[0].objectUUID); + }); + + it('should display the objectType in the fifth column', () => { + const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); + const el = rowElements[0].query(By.css('td:nth-child(5)')).nativeElement; + expect(el.textContent).toContain(audits.page[0].objectType); + }); + + it('should display the subjectUUID in the sixth column', () => { + const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); + const el = rowElements[0].query(By.css('td:nth-child(6)')).nativeElement; + expect(el.textContent).toContain(audits.page[0].subjectUUID); + }); + + it('should display the subjectType in the seventh column', () => { + const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); + const el = rowElements[0].query(By.css('td:nth-child(7)')).nativeElement; + expect(el.textContent).toContain(audits.page[0].subjectType); + }); + }); +}); diff --git a/src/app/audit-page/audit-table/audit-table.component.ts b/src/app/audit-page/audit-table/audit-table.component.ts index 14daa1feeb6..9e8a7385028 100644 --- a/src/app/audit-page/audit-table/audit-table.component.ts +++ b/src/app/audit-page/audit-table/audit-table.component.ts @@ -2,6 +2,7 @@ import { AsyncPipe, DatePipe, NgClass, + NgTemplateOutlet, } from '@angular/common'; import { ChangeDetectorRef, @@ -35,6 +36,7 @@ import { VarDirective } from '../../shared/utils/var.directive'; StringReplacePipe, NgClass, NgbCollapseModule, + NgTemplateOutlet, ], standalone: true, }) diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts b/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts new file mode 100644 index 00000000000..6e3f897ea3f --- /dev/null +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts @@ -0,0 +1,111 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { + ActivatedRoute, + Router, + RouterLink, +} from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { provideMockStore } from '@ngrx/store/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; +import { CollectionDataService } from 'src/app/core/data/collection-data.service'; +import { Item } from 'src/app/core/shared/item.model'; +import { APP_DATA_SERVICES_MAP } from 'src/config/app-config.interface'; + +import { AuditDataService } from '../../core/audit/audit-data.service'; +import { Audit } from '../../core/audit/model/audit.model'; +import { ItemDataService } from '../../core/data/item-data.service'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { MockActivatedRoute } from '../../shared/mocks/active-router.mock'; +import { RouterMock } from '../../shared/mocks/router.mock'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { AuditMock } from '../../shared/testing/audit.mock'; +import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; +import { createPaginatedList } from '../../shared/testing/utils.test'; +import { AuditTableComponent } from '../audit-table/audit-table.component'; +import { ObjectAuditOverviewComponent } from './object-audit-overview.component'; + +describe('ObjectAuditOverviewComponent', () => { + let component: ObjectAuditOverviewComponent; + let fixture: ComponentFixture; + + let auditService: AuditDataService; + let audits: Audit[]; + let itemService: ItemDataService; + let collectionService; + let activatedRoute; + + + function init() { + audits = [ AuditMock ]; + auditService = jasmine.createSpyObj('auditService', { + findByObject: createSuccessfulRemoteDataObject$(createPaginatedList(audits)), + getEpersonName: observableOf('Eperson Name'), + auditHasDetails: false, + }); + itemService = jasmine.createSpyObj('ItemService', { findById: createSuccessfulRemoteDataObject$(new Item()) }); + collectionService = jasmine.createSpyObj('CollectionDataService', + { findOwningCollectionFor: createSuccessfulRemoteDataObject$(createPaginatedList([{ id : 'collectionId' }])) }, + ); + activatedRoute = new MockActivatedRoute({ objectId: '1234' }); + activatedRoute.paramMap = observableOf({ + get: () => '1234', + }); + } + + beforeEach(waitForAsync(() => { + init(); + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + RouterTestingModule.withRoutes([]), + AuditTableComponent, + ObjectAuditOverviewComponent, + RouterLink, + ], + providers: [ + { provide: AuditDataService, useValue: auditService }, + { provide: PaginationService, useValue: new PaginationServiceStub() }, + { provide: ItemDataService, useValue: itemService }, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useValue: new RouterMock() }, + { provide: CollectionDataService, useValue: collectionService }, + { provide: APP_DATA_SERVICES_MAP, useValue: new Map() }, + provideMockStore({}), + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(ObjectAuditOverviewComponent, { + remove: { + imports: [AuditTableComponent], + }, + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ObjectAuditOverviewComponent); + component = fixture.componentInstance; + spyOn(component, 'setAudits').and.callThrough(); + fixture.detectChanges(); + }); + + describe('object detail data setting', () => { + it('should set audits on init', fakeAsync(() => { + tick(); + fixture.detectChanges(); + expect(component.setAudits).toHaveBeenCalled(); + })); + + it('should set owning collection', () => { + expect(component.owningCollection$).toBeTruthy(); + }); + }); +}); diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts index 1e2f1e84149..f4e75abe161 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts @@ -1,9 +1,7 @@ -import { - AsyncPipe, - DatePipe, -} from '@angular/common'; +import { AsyncPipe } from '@angular/common'; import { Component, + OnDestroy, OnInit, } from '@angular/core'; import { @@ -17,13 +15,13 @@ import { combineLatest, forkJoin, Observable, + Subscription, } from 'rxjs'; import { filter, map, mergeMap, switchMap, - take, } from 'rxjs/operators'; import { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../collection-page/collection-page.resolver'; @@ -32,24 +30,17 @@ import { AuditDataService, } from '../../core/audit/audit-data.service'; import { Audit } from '../../core/audit/model/audit.model'; -import { AuthService } from '../../core/auth/auth.service'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { SortDirection } from '../../core/cache/models/sort-options.model'; import { CollectionDataService } from '../../core/data/collection-data.service'; -import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { FindListOptions } from '../../core/data/find-list-options.model'; import { ItemDataService } from '../../core/data/item-data.service'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; import { PaginationService } from '../../core/pagination/pagination.service'; -import { redirectOn4xx } from '../../core/shared/authorized.operators'; import { Collection } from '../../core/shared/collection.model'; import { Item } from '../../core/shared/item.model'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; -import { PaginationComponent } from '../../shared/pagination/pagination.component'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { VarDirective } from '../../shared/utils/var.directive'; import { AuditTableComponent } from '../audit-table/audit-table.component'; /** * Component displaying a list of all audit about a object in a paginated table @@ -58,17 +49,14 @@ import { AuditTableComponent } from '../audit-table/audit-table.component'; selector: 'ds-object-audit-overview', templateUrl: './object-audit-overview.component.html', imports: [ - PaginationComponent, AsyncPipe, TranslateModule, - VarDirective, - RouterLink, - DatePipe, AuditTableComponent, + RouterLink, ], standalone: true, }) -export class ObjectAuditOverviewComponent implements OnInit { +export class ObjectAuditOverviewComponent implements OnInit, OnDestroy { /** * The object extracted from the route. @@ -108,22 +96,20 @@ export class ObjectAuditOverviewComponent implements OnInit { dataNotAvailable = AUDIT_PERSON_NOT_AVAILABLE; - constructor(protected authService: AuthService, - protected route: ActivatedRoute, + sub: Subscription; + + constructor(protected route: ActivatedRoute, protected router: Router, protected auditService: AuditDataService, protected itemService: ItemDataService, - protected authorizationService: AuthorizationDataService, protected paginationService: PaginationService, protected collectionDataService: CollectionDataService, - public dsoNameService: DSONameService, ) {} ngOnInit(): void { - this.route.paramMap.pipe( - mergeMap((paramMap: ParamMap) => this.itemService.findById(paramMap.get('objectId'))), + this.sub = this.route.paramMap.pipe( + switchMap((paramMap: ParamMap) => this.itemService.findById(paramMap.get('objectId'))), getFirstCompletedRemoteData(), - redirectOn4xx(this.router, this.authService), ).subscribe((rd) => { this.object = rd.payload; this.owningCollection$ = this.collectionDataService.findOwningCollectionFor( @@ -139,6 +125,12 @@ export class ObjectAuditOverviewComponent implements OnInit { }); } + ngOnDestroy(): void { + if (this.sub) { + this.sub.unsubscribe(); + } + } + /** * Send a request to fetch all audits for the current page */ @@ -189,17 +181,4 @@ export class ObjectAuditOverviewComponent implements OnInit { }), ); } - - isCurrentUserAdmin(): Observable { - return combineLatest([ - this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin), - this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin), - this.authorizationService.isAuthorized(FeatureID.AdministratorOf), - ]).pipe( - map(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin]) => { - return isCollectionAdmin || isCommunityAdmin || isSiteAdmin; - }), - take(1), - ); - } } diff --git a/src/app/audit-page/overview/audit-overview.component.spec.ts b/src/app/audit-page/overview/audit-overview.component.spec.ts index 1446ca60525..f1c84c3213a 100644 --- a/src/app/audit-page/overview/audit-overview.component.spec.ts +++ b/src/app/audit-page/overview/audit-overview.component.spec.ts @@ -4,21 +4,19 @@ import { TestBed, waitForAsync, } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; +import { provideMockStore } from '@ngrx/store/testing'; import { TranslateModule } from '@ngx-translate/core'; import { of } from 'rxjs'; import { AuditDataService } from '../../core/audit/audit-data.service'; import { Audit } from '../../core/audit/model/audit.model'; -import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { PaginationService } from '../../core/pagination/pagination.service'; -import { PaginationComponent } from '../../shared/pagination/pagination.component'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { AuditMock } from '../../shared/testing/audit.mock'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; import { createPaginatedList } from '../../shared/testing/utils.test'; -import { VarDirective } from '../../shared/utils/var.directive'; +import { AuditTableComponent } from '../audit-table/audit-table.component'; import { AuditOverviewComponent } from './audit-overview.component'; describe('AuditOverviewComponent', () => { @@ -26,128 +24,53 @@ describe('AuditOverviewComponent', () => { let fixture: ComponentFixture; let auditService: AuditDataService; - let authorizationService: any; let audits: Audit[]; const paginationService = new PaginationServiceStub(); function init() { - audits = [ AuditMock, AuditMock, AuditMock ]; - auditService = jasmine.createSpyObj('processService', { + audits = [ AuditMock ]; + auditService = jasmine.createSpyObj('auditService', { findAll: createSuccessfulRemoteDataObject$(createPaginatedList(audits)), getEpersonName: of('Eperson Name'), + auditHasDetails: false, }); - authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']); } beforeEach(waitForAsync(() => { init(); TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), VarDirective, AuditOverviewComponent], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), AuditTableComponent, AuditOverviewComponent], providers: [ { provide: AuditDataService, useValue: auditService }, - { provide: AuthorizationDataService, useValue: authorizationService }, { provide: PaginationService, useValue: paginationService }, + provideMockStore({}), ], schemas: [NO_ERRORS_SCHEMA], - }).overrideComponent(AuditOverviewComponent, { remove: { imports: [PaginationComponent] } }).compileComponents(); + }) + .overrideComponent(AuditOverviewComponent, { + remove: { + imports: [AuditTableComponent], + }, + }) + .compileComponents(); })); - describe('if the current user is an admin', () => { - - beforeEach(() => { - authorizationService.isAuthorized.and.callFake(() => of(true)); - - fixture = TestBed.createComponent(AuditOverviewComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - describe('table structure', () => { - let rowElements; - - beforeEach(() => { - rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); - }); - - it(`should contain 3 rows`, () => { - expect(rowElements.length).toEqual(3); - }); - - it('should display the audit IDs in the first column', () => { - rowElements.forEach((rowElement, index) => { - const el = rowElement.query(By.css('td:nth-child(1)')).nativeElement; - expect(el.textContent).toContain(audits[index].id); - }); - }); - - it('should display the entityType in the second column', () => { - rowElements.forEach((rowElement, index) => { - const el = rowElement.query(By.css('td:nth-child(2)')).nativeElement; - expect(el.textContent).toContain(audits[index].eventType); - }); - }); - - it('should display the objectUUID in the third column', () => { - rowElements.forEach((rowElement, index) => { - const el = rowElement.query(By.css('td:nth-child(3)')).nativeElement; - expect(el.textContent).toContain(audits[index].objectUUID); - }); - }); - - it('should display the objectType in the fourth column', () => { - rowElements.forEach((rowElement, index) => { - const el = rowElement.query(By.css('td:nth-child(4)')).nativeElement; - expect(el.textContent).toContain(audits[index].objectType); - }); - }); - - it('should display the subjectUUID in the fifth column', () => { - rowElements.forEach((rowElement, index) => { - const el = rowElement.query(By.css('td:nth-child(5)')).nativeElement; - expect(el.textContent).toContain(audits[index].subjectUUID); - }); - }); - - it('should display the subjectType in the sixth column', () => { - rowElements.forEach((rowElement, index) => { - const el = rowElement.query(By.css('td:nth-child(6)')).nativeElement; - expect(el.textContent).toContain(audits[index].subjectType); - }); - }); - - it('should display the eperson name in the seventh column', () => { - rowElements.forEach((rowElement, index) => { - const el = rowElement.query(By.css('td:nth-child(7)')).nativeElement; - expect(el.textContent).toContain('Eperson Name'); - }); - }); - - }); - + beforeEach(() => { + fixture = TestBed.createComponent(AuditOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); - describe('if the current user is not an admin', () => { - - beforeEach(() => { - authorizationService.isAuthorized.and.callFake(() => of(false)); - - fixture = TestBed.createComponent(AuditOverviewComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - describe('table structure', () => { - let rowElements; - - beforeEach(() => { - rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); + describe('audit page overview data settings', () => { + it('should set audits on init', (done) => { + component.auditsRD$.subscribe(auditsRD => { + expect(auditsRD).toBeTruthy(); + expect(auditsRD.payload.page.length).toBe(1); + const audit = auditsRD.payload.page[0]; + expect(audit.epersonName).toEqual('Eperson Name'); + expect(audit.hasDetails).toBeFalsy(); + done(); }); - - it(`should contain 0 rows`, () => { - expect(rowElements.length).toEqual(0); - }); - }); }); - }); diff --git a/src/app/audit-page/overview/audit-overview.component.ts b/src/app/audit-page/overview/audit-overview.component.ts index 3ade6dcbb7d..6e6f689eaba 100644 --- a/src/app/audit-page/overview/audit-overview.component.ts +++ b/src/app/audit-page/overview/audit-overview.component.ts @@ -1,13 +1,8 @@ -import { - AsyncPipe, - DatePipe, - JsonPipe, -} from '@angular/common'; +import { AsyncPipe } from '@angular/common'; import { Component, OnInit, } from '@angular/core'; -import { RouterLink } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { forkJoin, @@ -23,16 +18,12 @@ import { import { AuditDataService } from '../../core/audit/audit-data.service'; import { Audit } from '../../core/audit/model/audit.model'; import { SortDirection } from '../../core/cache/models/sort-options.model'; -import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { FindListOptions } from '../../core/data/find-list-options.model'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; import { PaginationService } from '../../core/pagination/pagination.service'; -import { PaginationComponent } from '../../shared/pagination/pagination.component'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { followLink } from '../../shared/utils/follow-link-config.model'; -import { VarDirective } from '../../shared/utils/var.directive'; import { AuditTableComponent } from '../audit-table/audit-table.component'; /** @@ -42,13 +33,8 @@ import { AuditTableComponent } from '../audit-table/audit-table.component'; selector: 'ds-audit-overview', templateUrl: './audit-overview.component.html', imports: [ - PaginationComponent, AsyncPipe, TranslateModule, - RouterLink, - VarDirective, - DatePipe, - JsonPipe, AuditTableComponent, ], standalone: true, @@ -90,7 +76,6 @@ export class AuditOverviewComponent implements OnInit { dateFormat = 'yyyy-MM-dd HH:mm:ss'; constructor(protected auditService: AuditDataService, - protected authorizationService: AuthorizationDataService, protected paginationService: PaginationService) { } @@ -136,9 +121,4 @@ export class AuditOverviewComponent implements OnInit { ); } - - isCurrentUserAdmin(): Observable { - return this.authorizationService.isAuthorized(FeatureID.AdministratorOf, undefined, undefined); - } - } diff --git a/src/app/shared/testing/audit.mock.ts b/src/app/shared/testing/audit.mock.ts index 746b7b5ce5d..5e1ed9ba158 100644 --- a/src/app/shared/testing/audit.mock.ts +++ b/src/app/shared/testing/audit.mock.ts @@ -57,6 +57,7 @@ export const AuditMock: Audit = Object.assign(new Audit(), { subjectType: 'ITEM', subjectUUID: '3a74fe2c-d353-4e33-9887-d50184662dd4', timeStamp: '2020-11-13T10:41:06.223+0000', + epersonName: AuditEPersonMock.name, type: 'auditevent', _embedded: { eperson: AuditEPersonMock, From 4bb3b8540493b4ab4cc41df127452be90286f53f Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Thu, 24 Jul 2025 14:52:51 +0200 Subject: [PATCH 08/22] [DURACOM-317] fix lint --- .../audit-page/audit-table/audit-table.component.ts | 12 ++++++------ .../object-audit-overview.component.spec.ts | 6 +++--- .../object-audit-overview.component.ts | 2 +- .../audit-page/overview/audit-overview.component.ts | 2 +- .../shared/menu/providers/audit-item.menu.spec.ts | 4 ++-- .../menu/providers/audit-overview.menu.spec.ts | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/app/audit-page/audit-table/audit-table.component.ts b/src/app/audit-page/audit-table/audit-table.component.ts index 9e8a7385028..1fda03d80d1 100644 --- a/src/app/audit-page/audit-table/audit-table.component.ts +++ b/src/app/audit-page/audit-table/audit-table.component.ts @@ -27,16 +27,16 @@ import { VarDirective } from '../../shared/utils/var.directive'; selector: 'ds-audit-table', templateUrl: './audit-table.component.html', imports: [ - PaginationComponent, AsyncPipe, - TranslateModule, - VarDirective, - RouterLink, DatePipe, - StringReplacePipe, - NgClass, NgbCollapseModule, + NgClass, NgTemplateOutlet, + PaginationComponent, + RouterLink, + StringReplacePipe, + TranslateModule, + VarDirective, ], standalone: true, }) diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts b/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts index 6e3f897ea3f..adb078e7043 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts @@ -14,7 +14,7 @@ import { import { RouterTestingModule } from '@angular/router/testing'; import { provideMockStore } from '@ngrx/store/testing'; import { TranslateModule } from '@ngx-translate/core'; -import { of as observableOf } from 'rxjs'; +import { of } from 'rxjs'; import { CollectionDataService } from 'src/app/core/data/collection-data.service'; import { Item } from 'src/app/core/shared/item.model'; import { APP_DATA_SERVICES_MAP } from 'src/config/app-config.interface'; @@ -47,7 +47,7 @@ describe('ObjectAuditOverviewComponent', () => { audits = [ AuditMock ]; auditService = jasmine.createSpyObj('auditService', { findByObject: createSuccessfulRemoteDataObject$(createPaginatedList(audits)), - getEpersonName: observableOf('Eperson Name'), + getEpersonName: of('Eperson Name'), auditHasDetails: false, }); itemService = jasmine.createSpyObj('ItemService', { findById: createSuccessfulRemoteDataObject$(new Item()) }); @@ -55,7 +55,7 @@ describe('ObjectAuditOverviewComponent', () => { { findOwningCollectionFor: createSuccessfulRemoteDataObject$(createPaginatedList([{ id : 'collectionId' }])) }, ); activatedRoute = new MockActivatedRoute({ objectId: '1234' }); - activatedRoute.paramMap = observableOf({ + activatedRoute.paramMap = of({ get: () => '1234', }); } diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts index f4e75abe161..effe6e61fcb 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts @@ -50,9 +50,9 @@ import { AuditTableComponent } from '../audit-table/audit-table.component'; templateUrl: './object-audit-overview.component.html', imports: [ AsyncPipe, - TranslateModule, AuditTableComponent, RouterLink, + TranslateModule, ], standalone: true, }) diff --git a/src/app/audit-page/overview/audit-overview.component.ts b/src/app/audit-page/overview/audit-overview.component.ts index 6e6f689eaba..2989b49780d 100644 --- a/src/app/audit-page/overview/audit-overview.component.ts +++ b/src/app/audit-page/overview/audit-overview.component.ts @@ -34,8 +34,8 @@ import { AuditTableComponent } from '../audit-table/audit-table.component'; templateUrl: './audit-overview.component.html', imports: [ AsyncPipe, - TranslateModule, AuditTableComponent, + TranslateModule, ], standalone: true, }) diff --git a/src/app/shared/menu/providers/audit-item.menu.spec.ts b/src/app/shared/menu/providers/audit-item.menu.spec.ts index 8a9cab85810..240ec0c4e3f 100644 --- a/src/app/shared/menu/providers/audit-item.menu.spec.ts +++ b/src/app/shared/menu/providers/audit-item.menu.spec.ts @@ -1,6 +1,6 @@ import { TestBed } from '@angular/core/testing'; -import { of as observableOf } from 'rxjs'; +import { of } from 'rxjs'; import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; @@ -49,7 +49,7 @@ describe('AuditLogsMenuProvider', () => { beforeEach(() => { spyOn(authorizationServiceStub, 'isAuthorized').and.returnValue( - observableOf(true), + of(true), ); TestBed.configureTestingModule({ providers: [ diff --git a/src/app/shared/menu/providers/audit-overview.menu.spec.ts b/src/app/shared/menu/providers/audit-overview.menu.spec.ts index 1c46c0915f7..e0d4b2e1fe1 100644 --- a/src/app/shared/menu/providers/audit-overview.menu.spec.ts +++ b/src/app/shared/menu/providers/audit-overview.menu.spec.ts @@ -7,7 +7,7 @@ */ import { TestBed } from '@angular/core/testing'; -import { of as observableOf } from 'rxjs'; +import { of } from 'rxjs'; import { APP_CONFIG } from '../../../../config/app-config.interface'; import { environment } from '../../../../environments/environment'; @@ -35,7 +35,7 @@ describe('AuditOverviewMenuProvider', () => { beforeEach(() => { spyOn(authorizationServiceStub, 'isAuthorized').and.returnValue( - observableOf(true), + of(true), ); TestBed.configureTestingModule({ From 16b47c2f84370b365863fb75f1bb9adbdbe545b7 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Thu, 24 Jul 2025 15:30:16 +0200 Subject: [PATCH 09/22] [DURACOM-317] fix expected timestamp --- src/app/audit-page/audit-table/audit-table.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/audit-page/audit-table/audit-table.component.spec.ts b/src/app/audit-page/audit-table/audit-table.component.spec.ts index 5191d56091e..088a946c38a 100644 --- a/src/app/audit-page/audit-table/audit-table.component.spec.ts +++ b/src/app/audit-page/audit-table/audit-table.component.spec.ts @@ -67,7 +67,7 @@ describe('AuditTableComponent', () => { it('should display the timestamp in the third column', () => { const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); const el = rowElements[0].query(By.css('td:nth-child(3)')).nativeElement; - expect(el.textContent).toContain('2020-11-13 11:41:06'); + expect(el.textContent).toContain('2020-11-13 10:41:06'); }); it('should display the objectUUID in the fourth column', () => { From 9f72031ca6d35916228ab4ac15b83f8672731296 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Mon, 15 Sep 2025 11:50:59 +0200 Subject: [PATCH 10/22] [DURACOM-317] fix tests --- src/app/audit-page/audit-table/audit-table.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/audit-page/audit-table/audit-table.component.spec.ts b/src/app/audit-page/audit-table/audit-table.component.spec.ts index 088a946c38a..5191d56091e 100644 --- a/src/app/audit-page/audit-table/audit-table.component.spec.ts +++ b/src/app/audit-page/audit-table/audit-table.component.spec.ts @@ -67,7 +67,7 @@ describe('AuditTableComponent', () => { it('should display the timestamp in the third column', () => { const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); const el = rowElements[0].query(By.css('td:nth-child(3)')).nativeElement; - expect(el.textContent).toContain('2020-11-13 10:41:06'); + expect(el.textContent).toContain('2020-11-13 11:41:06'); }); it('should display the objectUUID in the fourth column', () => { From c6745bd255da255c890ef4f36417a6aa6e72ad98 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Mon, 15 Sep 2025 12:18:01 +0200 Subject: [PATCH 11/22] [DURACOM-317] correct text fix --- src/app/audit-page/audit-table/audit-table.component.html | 2 +- src/app/audit-page/audit-table/audit-table.component.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/audit-page/audit-table/audit-table.component.html b/src/app/audit-page/audit-table/audit-table.component.html index bc673920269..23bc5f141a5 100644 --- a/src/app/audit-page/audit-table/audit-table.component.html +++ b/src/app/audit-page/audit-table/audit-table.component.html @@ -47,7 +47,7 @@ } {{ audit.epersonName }} - {{ audit.timeStamp | date:dateFormat}} + {{ audit.timeStamp | date:dateFormat:'UTC' }} @if (isOverviewPage) { @if (audit.objectUUID) { diff --git a/src/app/audit-page/audit-table/audit-table.component.spec.ts b/src/app/audit-page/audit-table/audit-table.component.spec.ts index 5191d56091e..088a946c38a 100644 --- a/src/app/audit-page/audit-table/audit-table.component.spec.ts +++ b/src/app/audit-page/audit-table/audit-table.component.spec.ts @@ -67,7 +67,7 @@ describe('AuditTableComponent', () => { it('should display the timestamp in the third column', () => { const rowElements = fixture.debugElement.queryAll(By.css('tbody tr')); const el = rowElements[0].query(By.css('td:nth-child(3)')).nativeElement; - expect(el.textContent).toContain('2020-11-13 11:41:06'); + expect(el.textContent).toContain('2020-11-13 10:41:06'); }); it('should display the objectUUID in the fourth column', () => { From a7b8519c389f96424e4593dcfc11bfc9f0f4bca3 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Thu, 25 Sep 2025 15:33:22 +0200 Subject: [PATCH 12/22] [DURACOM-317] fix/improve audit view --- .../audit-table/audit-table.component.html | 10 ++++-- .../object-audit-overview.component.html | 6 ++-- .../object-audit-overview.component.ts | 34 ++++++------------- src/app/core/audit/audit-data.service.ts | 9 +---- 4 files changed, 21 insertions(+), 38 deletions(-) diff --git a/src/app/audit-page/audit-table/audit-table.component.html b/src/app/audit-page/audit-table/audit-table.component.html index 23bc5f141a5..c15c3635810 100644 --- a/src/app/audit-page/audit-table/audit-table.component.html +++ b/src/app/audit-page/audit-table/audit-table.component.html @@ -14,10 +14,10 @@ {{ 'audit.overview.table.eperson' | translate }} {{ 'audit.overview.table.timestamp' | translate }} @if (isOverviewPage) { - {{ 'audit.overview.table.objectUUID' | translate }} - {{ 'audit.overview.table.objectType' | translate }} {{ 'audit.overview.table.subjectUUID' | translate }} {{ 'audit.overview.table.subjectType' | translate }} + {{ 'audit.overview.table.objectUUID' | translate }} + {{ 'audit.overview.table.objectType' | translate }} } @else { {{ 'audit.overview.table.other' | translate }} } @@ -55,7 +55,11 @@ } {{ audit.objectType }} - {{ audit.subjectUUID }} + + @if (audit.subjectUUID) { + {{audit.subjectUUID}} + } + {{ audit.subjectType }} } @else { diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html index 49c1aeb7e62..78f1a9478ac 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html @@ -3,8 +3,8 @@

{{'audit.object.overview.title' | translate}}

- @if (object) { -

{{ object.name }} ({{object.type}})

+ @if (objectId) { + @if ((auditsRD$ | async)?.payload; as audits) { {{ object.name }} ({{object.type}}) [object]="object" > } - {{ 'audit.object.back' | translate }} + {{ 'audit.object.back' | translate }} @if ((auditsRD$ | async)?.statusCode === 404) {

{{'audit.object.overview.disabled.message' | translate}}

} diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts index effe6e61fcb..afe16336be3 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts @@ -21,7 +21,7 @@ import { filter, map, mergeMap, - switchMap, + switchMap, tap, } from 'rxjs/operators'; import { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../collection-page/collection-page.resolver'; @@ -92,7 +92,7 @@ export class ObjectAuditOverviewComponent implements OnInit, OnDestroy { */ dateFormat = 'yyyy-MM-dd HH:mm:ss'; - owningCollection$: Observable; + objectId: string; dataNotAvailable = AUDIT_PERSON_NOT_AVAILABLE; @@ -108,19 +108,9 @@ export class ObjectAuditOverviewComponent implements OnInit, OnDestroy { ngOnInit(): void { this.sub = this.route.paramMap.pipe( - switchMap((paramMap: ParamMap) => this.itemService.findById(paramMap.get('objectId'))), - getFirstCompletedRemoteData(), - ).subscribe((rd) => { - this.object = rd.payload; - this.owningCollection$ = this.collectionDataService.findOwningCollectionFor( - this.object, - true, - false, - ...COLLECTION_PAGE_LINKS_TO_FOLLOW, - ).pipe( - getFirstCompletedRemoteData(), - map(data => data?.payload), - ); + map((paramMap: ParamMap) => paramMap.get('objectId')), + ).subscribe((id) => { + this.objectId = id; this.setAudits(); }); } @@ -136,15 +126,11 @@ export class ObjectAuditOverviewComponent implements OnInit, OnDestroy { */ setAudits() { const config$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config); - const parentCommunity$ = this.owningCollection$.pipe( - switchMap(collection => collection.parentCommunity), - getFirstCompletedRemoteData(), - map(data => data?.payload), - ); - this.auditsRD$ = combineLatest([ config$, this.owningCollection$, parentCommunity$]).pipe( - switchMap(([config, owningCollection, parentCommunity]) => - this.auditService.findByObject(this.object.id, config, owningCollection.id, parentCommunity.id).pipe( + this.auditsRD$ = config$.pipe( + tap(console.log), + switchMap((config) => + this.auditService.findByObject(this.objectId, config).pipe( getFirstCompletedRemoteData(), ), ), @@ -160,7 +146,7 @@ export class ObjectAuditOverviewComponent implements OnInit, OnDestroy { const updatedAudits$ = auditsRD.payload.page.map(audit => { return forkJoin({ epersonName: this.auditService.getEpersonName(audit), - otherAuditObject: this.auditService.getOtherObject(audit, this.object.id), + otherAuditObject: this.auditService.getOtherObject(audit, this.objectId), }).pipe( map(({ epersonName, otherAuditObject }) => Object.assign(new Audit(), audit, { epersonName, otherAuditObject }), diff --git a/src/app/core/audit/audit-data.service.ts b/src/app/core/audit/audit-data.service.ts index c2a88b50023..c4d799d4040 100644 --- a/src/app/core/audit/audit-data.service.ts +++ b/src/app/core/audit/audit-data.service.ts @@ -66,17 +66,10 @@ export class AuditDataService extends IdentifiableDataService{ * @param commUuid The Uuid of the community * @return Observable>> */ - findByObject(objectId: string, options: FindListOptions = {}, collUuid?: string, commUuid?: string): Observable>> { + findByObject(objectId: string, options: FindListOptions = {}): Observable>> { const searchMethod = AUDIT_FIND_BY_OBJECT_SEARCH_METHOD; const searchParams = [new RequestParam('object', objectId)]; - if (hasValue(commUuid)) { - searchParams.push(new RequestParam('commUuid', commUuid)); - } - - if (hasValue(collUuid)) { - searchParams.push(new RequestParam('collUuid', collUuid)); - } const optionsWithObject = Object.assign(new FindListOptions(), options, { searchParams, }); From fe25c5500268e3cd79f26f1e6b742664bb862780 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Thu, 25 Sep 2025 15:49:01 +0200 Subject: [PATCH 13/22] [DURACOM-317] audit view improvement --- .../object-audit-overview.component.html | 2 +- .../object-audit-overview.component.ts | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html index 78f1a9478ac..0a0fedfe7b6 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html @@ -4,7 +4,7 @@

{{'audit.object.overview.title' | translate}}

@if (objectId) { - +

{{ objectName }}

@if ((auditsRD$ | async)?.payload; as audits) { paramMap.get('objectId')), - ).subscribe((id) => { - this.objectId = id; + switchMap((id: string) => this.dSpaceObjectDataService.findById(id, true, true)), + getFirstSucceededRemoteDataPayload(), + ).subscribe((dso) => { + this.objectId = dso.id; + this.objectName = this.dsoNameService.getName(dso); this.setAudits(); }); } @@ -128,7 +137,6 @@ export class ObjectAuditOverviewComponent implements OnInit, OnDestroy { const config$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config); this.auditsRD$ = config$.pipe( - tap(console.log), switchMap((config) => this.auditService.findByObject(this.objectId, config).pipe( getFirstCompletedRemoteData(), From a1b602e76b55b31b907904a34cd242816bb504bf Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Fri, 26 Sep 2025 12:04:18 +0200 Subject: [PATCH 14/22] [DURACOM-317] refactor, fix other object reference --- src/app/app.menus.ts | 2 + .../audit-table/audit-table.component.html | 18 ++--- .../object-audit-overview.component.html | 10 ++- .../object-audit-overview.component.spec.ts | 31 ++++++--- .../object-audit-overview.component.ts | 65 ++++++++++--------- .../overview/audit-overview.component.ts | 2 +- src/app/core/audit/audit-data.service.ts | 7 +- src/assets/i18n/en.json5 | 2 +- 8 files changed, 78 insertions(+), 59 deletions(-) diff --git a/src/app/app.menus.ts b/src/app/app.menus.ts index cd54cde1f8e..e230b039719 100644 --- a/src/app/app.menus.ts +++ b/src/app/app.menus.ts @@ -94,6 +94,8 @@ export const MENUS = buildMenuStructure({ MenuRoute.ITEM_PAGE, ), AuditLogsMenuProvider.onRoute( + MenuRoute.COMMUNITY_PAGE, + MenuRoute.COLLECTION_PAGE, MenuRoute.ITEM_PAGE, ), OrcidMenuProvider.onRoute( diff --git a/src/app/audit-page/audit-table/audit-table.component.html b/src/app/audit-page/audit-table/audit-table.component.html index c15c3635810..4ed0b609663 100644 --- a/src/app/audit-page/audit-table/audit-table.component.html +++ b/src/app/audit-page/audit-table/audit-table.component.html @@ -63,17 +63,13 @@ {{ audit.subjectType }} } @else { - @if (object && object.id === audit.objectUUID) { - - @if (audit.otherAuditObject; as dso) { - {{ dsoNameService.getName(dso) }} ({{ dso.type }}) - } @else { - {{ dataNotAvailable }} - } - - } @else { - {{ dataNotAvailable }} - } + + @if (audit.otherAuditObject; as dso) { + {{ dsoNameService.getName(dso) }} ({{ dso.type }}) + } @else { + {{ dataNotAvailable }} + } + } diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html index 0a0fedfe7b6..ac42f242c64 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html @@ -3,8 +3,10 @@

{{'audit.object.overview.title' | translate}}

- @if (objectId) { -

{{ objectName }}

+ @if (objectId$ | async) { + @if ((auditsRD$ | async)?.payload; as audits) { {{ objectName }} [object]="object" > } - {{ 'audit.object.back' | translate }} + @if ((auditsRD$ | async)?.statusCode === 404) {

{{'audit.object.overview.disabled.message' | translate}}

} diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts b/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts index adb078e7043..8814a37bb21 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts @@ -1,3 +1,4 @@ +import { Location } from '@angular/common'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, @@ -21,7 +22,7 @@ import { APP_DATA_SERVICES_MAP } from 'src/config/app-config.interface'; import { AuditDataService } from '../../core/audit/audit-data.service'; import { Audit } from '../../core/audit/model/audit.model'; -import { ItemDataService } from '../../core/data/item-data.service'; +import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { PaginationService } from '../../core/pagination/pagination.service'; import { MockActivatedRoute } from '../../shared/mocks/active-router.mock'; import { RouterMock } from '../../shared/mocks/router.mock'; @@ -38,10 +39,13 @@ describe('ObjectAuditOverviewComponent', () => { let auditService: AuditDataService; let audits: Audit[]; - let itemService: ItemDataService; + let dSpaceObjectDataService: DSpaceObjectDataService; let collectionService; let activatedRoute; - + let locationStub: Location; + const mockItem = new Item(); + const mockItemId = '1234'; + mockItem.id = mockItemId; function init() { audits = [ AuditMock ]; @@ -49,14 +53,18 @@ describe('ObjectAuditOverviewComponent', () => { findByObject: createSuccessfulRemoteDataObject$(createPaginatedList(audits)), getEpersonName: of('Eperson Name'), auditHasDetails: false, + getOtherObject: of(new Audit()), }); - itemService = jasmine.createSpyObj('ItemService', { findById: createSuccessfulRemoteDataObject$(new Item()) }); + dSpaceObjectDataService = jasmine.createSpyObj('DSpaceObjectDataService', { findById: createSuccessfulRemoteDataObject$(mockItem) }); collectionService = jasmine.createSpyObj('CollectionDataService', { findOwningCollectionFor: createSuccessfulRemoteDataObject$(createPaginatedList([{ id : 'collectionId' }])) }, ); - activatedRoute = new MockActivatedRoute({ objectId: '1234' }); + activatedRoute = new MockActivatedRoute({ objectId: mockItemId }); activatedRoute.paramMap = of({ - get: () => '1234', + get: () => mockItemId, + }); + locationStub = jasmine.createSpyObj('location', { + back: jasmine.createSpy('back'), }); } @@ -73,11 +81,12 @@ describe('ObjectAuditOverviewComponent', () => { providers: [ { provide: AuditDataService, useValue: auditService }, { provide: PaginationService, useValue: new PaginationServiceStub() }, - { provide: ItemDataService, useValue: itemService }, + { provide: DSpaceObjectDataService, useValue: dSpaceObjectDataService }, { provide: ActivatedRoute, useValue: activatedRoute }, { provide: Router, useValue: new RouterMock() }, { provide: CollectionDataService, useValue: collectionService }, { provide: APP_DATA_SERVICES_MAP, useValue: new Map() }, + { provide: Location, useValue: locationStub }, provideMockStore({}), ], schemas: [NO_ERRORS_SCHEMA], @@ -104,8 +113,12 @@ describe('ObjectAuditOverviewComponent', () => { expect(component.setAudits).toHaveBeenCalled(); })); - it('should set owning collection', () => { - expect(component.owningCollection$).toBeTruthy(); + it('should set object id', (done) => { + component.objectId$.subscribe((id) => { + expect(id).toEqual(mockItemId); + expect(component.objectId).toEqual(id); + done(); + }); }); }); }); diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts index a994383e6ec..d467662c152 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts @@ -1,7 +1,9 @@ -import { AsyncPipe } from '@angular/common'; +import { + AsyncPipe, + Location, +} from '@angular/common'; import { Component, - OnDestroy, OnInit, } from '@angular/core'; import { @@ -12,38 +14,38 @@ import { } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { - combineLatest, forkJoin, Observable, - Subscription, } from 'rxjs'; import { filter, map, mergeMap, - switchMap, tap, + switchMap, + tap, } from 'rxjs/operators'; -import { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../collection-page/collection-page.resolver'; +import { getDSORoute } from '../../app-routing-paths'; import { AUDIT_PERSON_NOT_AVAILABLE, AuditDataService, } from '../../core/audit/audit-data.service'; import { Audit } from '../../core/audit/model/audit.model'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { SortDirection } from '../../core/cache/models/sort-options.model'; import { CollectionDataService } from '../../core/data/collection-data.service'; +import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { FindListOptions } from '../../core/data/find-list-options.model'; -import { ItemDataService } from '../../core/data/item-data.service'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; import { PaginationService } from '../../core/pagination/pagination.service'; -import { Collection } from '../../core/shared/collection.model'; -import { Item } from '../../core/shared/item.model'; -import {getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload} from '../../core/shared/operators'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, +} from '../../core/shared/operators'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { AuditTableComponent } from '../audit-table/audit-table.component'; -import {DSONameService} from '../../core/breadcrumbs/dso-name.service'; -import {DSpaceObjectDataService} from '../../core/data/dspace-object-data.service'; /** * Component displaying a list of all audit about a object in a paginated table */ @@ -58,12 +60,12 @@ import {DSpaceObjectDataService} from '../../core/data/dspace-object-data.servic ], standalone: true, }) -export class ObjectAuditOverviewComponent implements OnInit, OnDestroy { +export class ObjectAuditOverviewComponent implements OnInit { /** * The object extracted from the route. */ - object: Item; + object: DSpaceObject; /** * List of all audits @@ -94,40 +96,39 @@ export class ObjectAuditOverviewComponent implements OnInit, OnDestroy { */ dateFormat = 'yyyy-MM-dd HH:mm:ss'; + objectId$: Observable; + objectId: string; objectName: string; - dataNotAvailable = AUDIT_PERSON_NOT_AVAILABLE; + objectRoute: string; - sub: Subscription; + dataNotAvailable = AUDIT_PERSON_NOT_AVAILABLE; constructor(protected route: ActivatedRoute, protected router: Router, protected auditService: AuditDataService, - protected itemService: ItemDataService, protected paginationService: PaginationService, protected collectionDataService: CollectionDataService, protected dsoNameService: DSONameService, protected dSpaceObjectDataService: DSpaceObjectDataService, + protected location: Location, ) {} ngOnInit(): void { - this.sub = this.route.paramMap.pipe( + this.objectId$ = this.route.paramMap.pipe( map((paramMap: ParamMap) => paramMap.get('objectId')), switchMap((id: string) => this.dSpaceObjectDataService.findById(id, true, true)), getFirstSucceededRemoteDataPayload(), - ).subscribe((dso) => { - this.objectId = dso.id; - this.objectName = this.dsoNameService.getName(dso); - this.setAudits(); - }); - } - - ngOnDestroy(): void { - if (this.sub) { - this.sub.unsubscribe(); - } + tap((object) => { + this.objectRoute = getDSORoute(object); + this.objectId = object.id; + this.objectName = this.dsoNameService.getName(object); + this.setAudits(); + }), + map(dso => dso.id), + ); } /** @@ -138,7 +139,7 @@ export class ObjectAuditOverviewComponent implements OnInit, OnDestroy { this.auditsRD$ = config$.pipe( switchMap((config) => - this.auditService.findByObject(this.objectId, config).pipe( + this.auditService.findByObject(this.objectId, config, false).pipe( getFirstCompletedRemoteData(), ), ), @@ -175,4 +176,8 @@ export class ObjectAuditOverviewComponent implements OnInit, OnDestroy { }), ); } + + goBack(): void { + this.location.back(); + } } diff --git a/src/app/audit-page/overview/audit-overview.component.ts b/src/app/audit-page/overview/audit-overview.component.ts index 2989b49780d..e08bb57be3e 100644 --- a/src/app/audit-page/overview/audit-overview.component.ts +++ b/src/app/audit-page/overview/audit-overview.component.ts @@ -89,7 +89,7 @@ export class AuditOverviewComponent implements OnInit { setAudits() { this.auditsRD$ = this.paginationService.getFindListOptions(this.pageId, this.config).pipe( switchMap((config) => { - return this.auditService.findAll(config, true, true, followLink('eperson')); + return this.auditService.findAll(config, false, true, followLink('eperson')); }), filter(data => data && data?.payload?.page?.length > 0), map((audits) => { diff --git a/src/app/core/audit/audit-data.service.ts b/src/app/core/audit/audit-data.service.ts index c4d799d4040..e3279853eda 100644 --- a/src/app/core/audit/audit-data.service.ts +++ b/src/app/core/audit/audit-data.service.ts @@ -62,18 +62,17 @@ export class AuditDataService extends IdentifiableDataService{ * * @param objectId The objectId id * @param options The [[FindListOptions]] object - * @param collUuid The Uuid of the collection - * @param commUuid The Uuid of the community + * @param useCachedVersionIfAvailable * @return Observable>> */ - findByObject(objectId: string, options: FindListOptions = {}): Observable>> { + findByObject(objectId: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true): Observable>> { const searchMethod = AUDIT_FIND_BY_OBJECT_SEARCH_METHOD; const searchParams = [new RequestParam('object', objectId)]; const optionsWithObject = Object.assign(new FindListOptions(), options, { searchParams, }); - return this.searchData.searchBy(searchMethod, optionsWithObject, true, true, followLink('eperson')); + return this.searchData.searchBy(searchMethod, optionsWithObject, useCachedVersionIfAvailable, true, followLink('eperson')); } /** diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 3670d3d696e..052a3e8b327 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -957,7 +957,7 @@ "audit.overview.breadcrumbs": "Audit Logs Overview", - "audit.object.back": "Back to Item", + "audit.object.back": "Back", "audit.object.breadcrumbs": "Subject Audit Logs", From 63ed343dd7fedd48fdb2645ad3f5c0c26816cef9 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Wed, 12 Nov 2025 12:32:43 +0100 Subject: [PATCH 15/22] [DURACOM-317] fix conflicts, adapt paths --- .../audit-table/audit-table.component.spec.ts | 12 +++---- .../audit-table/audit-table.component.ts | 4 +-- .../object-audit-overview.component.spec.ts | 26 +++++++------- .../object-audit-overview.component.ts | 12 +++---- .../overview/audit-overview.component.spec.ts | 14 ++++---- .../overview/audit-overview.component.ts | 6 ++-- src/app/core/data-services-map.ts | 4 +-- .../audit-data.service.spec.ts | 22 ++++++------ .../{audit => data}/audit-data.service.ts | 36 +++++++++---------- src/app/core/testing/audit.mock.ts | 4 +-- .../menu/providers/audit-item.menu.spec.ts | 16 ++++----- .../shared/menu/providers/audit-item.menu.ts | 14 ++++---- .../providers/audit-overview.menu.spec.ts | 6 ++-- .../menu/providers/audit-overview.menu.ts | 12 +++---- src/app/shared/utils/string-replace.pipe.ts | 2 +- 15 files changed, 95 insertions(+), 95 deletions(-) rename src/app/core/{audit => data}/audit-data.service.spec.ts (91%) rename src/app/core/{audit => data}/audit-data.service.ts (88%) diff --git a/src/app/audit-page/audit-table/audit-table.component.spec.ts b/src/app/audit-page/audit-table/audit-table.component.spec.ts index 088a946c38a..3e1b8c54b2c 100644 --- a/src/app/audit-page/audit-table/audit-table.component.spec.ts +++ b/src/app/audit-page/audit-table/audit-table.component.spec.ts @@ -6,14 +6,14 @@ import { } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; +import { Audit } from '@dspace/core/audit/model/audit.model'; +import { DSONameService } from '@dspace/core/breadcrumbs/dso-name.service'; +import { PaginatedList } from '@dspace/core/data/paginated-list.model'; +import { AuditMock } from '@dspace/core/testing/audit.mock'; +import { DSONameServiceMock } from '@dspace/core/testing/dso-name.service.mock'; import { TranslateModule } from '@ngx-translate/core'; -import { PaginatedList } from 'src/app/core/data/paginated-list.model'; +import { PaginationComponent } from 'src/app/shared/pagination/pagination.component'; -import { Audit } from '../../core/audit/model/audit.model'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; -import { DSONameServiceMock } from '../../shared/mocks/dso-name.service.mock'; -import { PaginationComponent } from '../../shared/pagination/pagination.component'; -import { AuditMock } from '../../shared/testing/audit.mock'; import { AuditTableComponent } from './audit-table.component'; describe('AuditTableComponent', () => { diff --git a/src/app/audit-page/audit-table/audit-table.component.ts b/src/app/audit-page/audit-table/audit-table.component.ts index 1fda03d80d1..8768230ea5b 100644 --- a/src/app/audit-page/audit-table/audit-table.component.ts +++ b/src/app/audit-page/audit-table/audit-table.component.ts @@ -10,16 +10,16 @@ import { Input, } from '@angular/core'; import { RouterLink } from '@angular/router'; +import { AUDIT_PERSON_NOT_AVAILABLE } from '@dspace/core/data/audit-data.service'; +import { PaginationComponentOptions } from '@dspace/core/pagination/pagination-component-options.model'; import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; -import { AUDIT_PERSON_NOT_AVAILABLE } from '../../core/audit/audit-data.service'; import { Audit } from '../../core/audit/model/audit.model'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { PaginationComponent } from '../../shared/pagination/pagination.component'; -import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { StringReplacePipe } from '../../shared/utils/string-replace.pipe'; import { VarDirective } from '../../shared/utils/var.directive'; diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts b/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts index 8814a37bb21..88a0b59a961 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts @@ -13,23 +13,23 @@ import { RouterLink, } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; +import { Audit } from '@dspace/core/audit/model/audit.model'; +import { AuditDataService } from '@dspace/core/data/audit-data.service'; +import { CollectionDataService } from '@dspace/core/data/collection-data.service'; +import { DSpaceObjectDataService } from '@dspace/core/data/dspace-object-data.service'; +import { APP_DATA_SERVICES_MAP } from '@dspace/core/data-services-map-type'; +import { PaginationService } from '@dspace/core/pagination/pagination.service'; +import { Item } from '@dspace/core/shared/item.model'; +import { MockActivatedRoute } from '@dspace/core/testing/active-router.mock'; +import { AuditMock } from '@dspace/core/testing/audit.mock'; +import { PaginationServiceStub } from '@dspace/core/testing/pagination-service.stub'; +import { RouterMock } from '@dspace/core/testing/router.mock'; +import { createPaginatedList } from '@dspace/core/testing/utils.test'; +import { createSuccessfulRemoteDataObject$ } from '@dspace/core/utilities/remote-data.utils'; import { provideMockStore } from '@ngrx/store/testing'; import { TranslateModule } from '@ngx-translate/core'; import { of } from 'rxjs'; -import { CollectionDataService } from 'src/app/core/data/collection-data.service'; -import { Item } from 'src/app/core/shared/item.model'; -import { APP_DATA_SERVICES_MAP } from 'src/config/app-config.interface'; -import { AuditDataService } from '../../core/audit/audit-data.service'; -import { Audit } from '../../core/audit/model/audit.model'; -import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; -import { PaginationService } from '../../core/pagination/pagination.service'; -import { MockActivatedRoute } from '../../shared/mocks/active-router.mock'; -import { RouterMock } from '../../shared/mocks/router.mock'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { AuditMock } from '../../shared/testing/audit.mock'; -import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; -import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuditTableComponent } from '../audit-table/audit-table.component'; import { ObjectAuditOverviewComponent } from './object-audit-overview.component'; diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts index d467662c152..19492e62213 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts @@ -12,6 +12,12 @@ import { Router, RouterLink, } from '@angular/router'; +import { + AUDIT_PERSON_NOT_AVAILABLE, + AuditDataService, +} from '@dspace/core/data/audit-data.service'; +import { PaginationComponentOptions } from '@dspace/core/pagination/pagination-component-options.model'; +import { getDSORoute } from '@dspace/core/router/utils/dso-route.utils'; import { TranslateModule } from '@ngx-translate/core'; import { forkJoin, @@ -25,11 +31,6 @@ import { tap, } from 'rxjs/operators'; -import { getDSORoute } from '../../app-routing-paths'; -import { - AUDIT_PERSON_NOT_AVAILABLE, - AuditDataService, -} from '../../core/audit/audit-data.service'; import { Audit } from '../../core/audit/model/audit.model'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { SortDirection } from '../../core/cache/models/sort-options.model'; @@ -44,7 +45,6 @@ import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload, } from '../../core/shared/operators'; -import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { AuditTableComponent } from '../audit-table/audit-table.component'; /** * Component displaying a list of all audit about a object in a paginated table diff --git a/src/app/audit-page/overview/audit-overview.component.spec.ts b/src/app/audit-page/overview/audit-overview.component.spec.ts index f1c84c3213a..d1b6bdfced5 100644 --- a/src/app/audit-page/overview/audit-overview.component.spec.ts +++ b/src/app/audit-page/overview/audit-overview.component.spec.ts @@ -5,17 +5,17 @@ import { waitForAsync, } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; +import { Audit } from '@dspace/core/audit/model/audit.model'; +import { AuditDataService } from '@dspace/core/data/audit-data.service'; +import { PaginationService } from '@dspace/core/pagination/pagination.service'; +import { AuditMock } from '@dspace/core/testing/audit.mock'; +import { PaginationServiceStub } from '@dspace/core/testing/pagination-service.stub'; +import { createPaginatedList } from '@dspace/core/testing/utils.test'; +import { createSuccessfulRemoteDataObject$ } from '@dspace/core/utilities/remote-data.utils'; import { provideMockStore } from '@ngrx/store/testing'; import { TranslateModule } from '@ngx-translate/core'; import { of } from 'rxjs'; -import { AuditDataService } from '../../core/audit/audit-data.service'; -import { Audit } from '../../core/audit/model/audit.model'; -import { PaginationService } from '../../core/pagination/pagination.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { AuditMock } from '../../shared/testing/audit.mock'; -import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; -import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuditTableComponent } from '../audit-table/audit-table.component'; import { AuditOverviewComponent } from './audit-overview.component'; diff --git a/src/app/audit-page/overview/audit-overview.component.ts b/src/app/audit-page/overview/audit-overview.component.ts index e08bb57be3e..b400bd258fa 100644 --- a/src/app/audit-page/overview/audit-overview.component.ts +++ b/src/app/audit-page/overview/audit-overview.component.ts @@ -3,6 +3,9 @@ import { Component, OnInit, } from '@angular/core'; +import { AuditDataService } from '@dspace/core/data/audit-data.service'; +import { PaginationComponentOptions } from '@dspace/core/pagination/pagination-component-options.model'; +import { followLink } from '@dspace/core/shared/follow-link-config.model'; import { TranslateModule } from '@ngx-translate/core'; import { forkJoin, @@ -15,15 +18,12 @@ import { mergeMap, } from 'rxjs/operators'; -import { AuditDataService } from '../../core/audit/audit-data.service'; import { Audit } from '../../core/audit/model/audit.model'; import { SortDirection } from '../../core/cache/models/sort-options.model'; import { FindListOptions } from '../../core/data/find-list-options.model'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; import { PaginationService } from '../../core/pagination/pagination.service'; -import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { followLink } from '../../shared/utils/follow-link-config.model'; import { AuditTableComponent } from '../audit-table/audit-table.component'; /** diff --git a/src/app/core/data-services-map.ts b/src/app/core/data-services-map.ts index 9a940c414f8..77f39e080dd 100644 --- a/src/app/core/data-services-map.ts +++ b/src/app/core/data-services-map.ts @@ -1,3 +1,4 @@ +import { AUDIT } from './audit/model/audit.resource-type'; import { LDN_SERVICE, LDN_SERVICE_CONSTRAINT_FILTERS, @@ -29,7 +30,6 @@ import { RESEARCHER_PROFILE } from './profile/model/researcher-profile.resource- import { RESOURCE_POLICY } from './resource-policy/models/resource-policy.resource-type'; import { ACCESS_STATUS } from './shared/access-status.resource-type'; import { ADMIN_NOTIFY_MESSAGE } from './shared/admin-notify-message.resource-type'; -import { AUDIT } from './audit/model/audit.resource-type'; import { AUTHORIZATION } from './shared/authorization.resource-type'; import { BITSTREAM } from './shared/bitstream.resource-type'; import { BITSTREAM_FORMAT } from './shared/bitstream-format.resource-type'; @@ -137,5 +137,5 @@ export const LAZY_DATA_SERVICES: LazyDataServicesMap = new Map([ [SUGGESTION_TARGET.value, () => import('./notifications/suggestions/target/suggestion-target-data.service').then(m => m.SuggestionTargetDataService)], [DUPLICATE.value, () => import('./submission/submission-duplicate-data.service').then(m => m.SubmissionDuplicateDataService)], [CorrectionType.type.value, () => import('./submission/correctiontype-data.service').then(m => m.CorrectionTypeDataService)], - [AUDIT.value, () => import('./audit/audit-data.service').then(m => m.AuditDataService)], + [AUDIT.value, () => import('./data/audit-data.service').then(m => m.AuditDataService)], ]); diff --git a/src/app/core/audit/audit-data.service.spec.ts b/src/app/core/data/audit-data.service.spec.ts similarity index 91% rename from src/app/core/audit/audit-data.service.spec.ts rename to src/app/core/data/audit-data.service.spec.ts index 62dc47e4e2f..485bb818fd5 100644 --- a/src/app/core/audit/audit-data.service.spec.ts +++ b/src/app/core/data/audit-data.service.spec.ts @@ -11,24 +11,24 @@ import { TranslateModule, } from '@ngx-translate/core'; -import { getMockRequestService } from '../../shared/mocks/request.service.mock'; -import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { AuditMock } from '../../shared/testing/audit.mock'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { RequestParam } from '../cache/models/request-param.model'; +import { CoreState } from '../core-state.model'; +import { followLink } from '../shared/follow-link-config.model'; +import { AuditMock } from '../testing/audit.mock'; +import { HALEndpointServiceStub } from '../testing/hal-endpoint-service.stub'; +import { getMockRequestService } from '../testing/request.service.mock'; +import { TranslateLoaderMock } from '../testing/translate-loader.mock'; import { createPaginatedList, createRequestEntry$, -} from '../../shared/testing/utils.test'; -import { followLink } from '../../shared/utils/follow-link-config.model'; -import { RequestParam } from '../cache/models/request-param.model'; -import { CoreState } from '../core-state.model'; -import { FindListOptions } from '../data/find-list-options.model'; -import { RequestService } from '../data/request.service'; +} from '../testing/utils.test'; +import { createSuccessfulRemoteDataObject$ } from '../utilities/remote-data.utils'; import { AUDIT_FIND_BY_OBJECT_SEARCH_METHOD, AuditDataService, } from './audit-data.service'; +import { FindListOptions } from './find-list-options.model'; +import { RequestService } from './request.service'; describe('AuditDataService', () => { let service: AuditDataService; diff --git a/src/app/core/audit/audit-data.service.ts b/src/app/core/data/audit-data.service.ts similarity index 88% rename from src/app/core/audit/audit-data.service.ts rename to src/app/core/data/audit-data.service.ts index e3279853eda..bebcbddcd6f 100644 --- a/src/app/core/audit/audit-data.service.ts +++ b/src/app/core/data/audit-data.service.ts @@ -1,35 +1,35 @@ import { Injectable } from '@angular/core'; +import { NotificationsService } from '@dspace/core/notification-system/notifications.service'; +import { + followLink, + FollowLinkConfig, +} from '@dspace/core/shared/follow-link-config.model'; +import { hasValue } from '@dspace/shared/utils/empty.util'; import { Observable, of, } from 'rxjs'; import { map } from 'rxjs/operators'; -import { hasValue } from '../../shared/empty.util'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { - followLink, - FollowLinkConfig, -} from '../../shared/utils/follow-link-config.model'; +import { Audit } from '../audit/model/audit.model'; import { DSONameService } from '../breadcrumbs/dso-name.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { DeleteDataImpl } from '../data/base/delete-data'; -import { - FindAllData, - FindAllDataImpl, -} from '../data/base/find-all-data'; -import { IdentifiableDataService } from '../data/base/identifiable-data.service'; -import { SearchDataImpl } from '../data/base/search-data'; -import { FindListOptions } from '../data/find-list-options.model'; -import { PaginatedList } from '../data/paginated-list.model'; -import { RemoteData } from '../data/remote-data'; -import { RequestService } from '../data/request.service'; import { EPerson } from '../eperson/models/eperson.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { getFirstCompletedRemoteData } from '../shared/operators'; -import { Audit } from './model/audit.model'; +import { DeleteDataImpl } from './base/delete-data'; +import { + FindAllData, + FindAllDataImpl, +} from './base/find-all-data'; +import { IdentifiableDataService } from './base/identifiable-data.service'; +import { SearchDataImpl } from './base/search-data'; +import { FindListOptions } from './find-list-options.model'; +import { PaginatedList } from './paginated-list.model'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; export const AUDIT_PERSON_NOT_AVAILABLE = 'n/a'; diff --git a/src/app/core/testing/audit.mock.ts b/src/app/core/testing/audit.mock.ts index 5e1ed9ba158..08a379bb557 100644 --- a/src/app/core/testing/audit.mock.ts +++ b/src/app/core/testing/audit.mock.ts @@ -1,5 +1,5 @@ -import { Audit } from '../../core/audit/model/audit.model'; -import { EPerson } from '../../core/eperson/models/eperson.model'; +import { Audit } from '@dspace/core/audit/model/audit.model'; +import { EPerson } from '@dspace/core/eperson/models/eperson.model'; export const AuditEPersonMock: EPerson = Object.assign(new EPerson(), { handle: null, diff --git a/src/app/shared/menu/providers/audit-item.menu.spec.ts b/src/app/shared/menu/providers/audit-item.menu.spec.ts index 240ec0c4e3f..a86a33c32f8 100644 --- a/src/app/shared/menu/providers/audit-item.menu.spec.ts +++ b/src/app/shared/menu/providers/audit-item.menu.spec.ts @@ -1,15 +1,15 @@ import { TestBed } from '@angular/core/testing'; +import { ConfigurationDataService } from '@dspace/core/data/configuration-data.service'; +import { AuthorizationDataService } from '@dspace/core/data/feature-authorization/authorization-data.service'; +import { Collection } from '@dspace/core/shared/collection.model'; +import { COLLECTION } from '@dspace/core/shared/collection.resource-type'; +import { ConfigurationProperty } from '@dspace/core/shared/configuration-property.model'; +import { AuthorizationDataServiceStub } from '@dspace/core/testing/authorization-service.stub'; +import { URLCombiner } from '@dspace/core/url-combiner/url-combiner'; +import { createSuccessfulRemoteDataObject$ } from '@dspace/core/utilities/remote-data.utils'; import { of } from 'rxjs'; -import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; -import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; -import { Collection } from '../../../core/shared/collection.model'; -import { COLLECTION } from '../../../core/shared/collection.resource-type'; -import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; -import { URLCombiner } from '../../../core/url-combiner/url-combiner'; -import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; -import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { MenuItemType } from '../menu-item-type.model'; import { PartialMenuSection } from '../menu-provider.model'; import { AuditLogsMenuProvider } from './audit-item.menu'; diff --git a/src/app/shared/menu/providers/audit-item.menu.ts b/src/app/shared/menu/providers/audit-item.menu.ts index fedcfd59ed9..590a73e9083 100644 --- a/src/app/shared/menu/providers/audit-item.menu.ts +++ b/src/app/shared/menu/providers/audit-item.menu.ts @@ -6,19 +6,19 @@ * http://www.dspace.org/license/ */ import { Injectable } from '@angular/core'; +import { ConfigurationDataService } from '@dspace/core/data/configuration-data.service'; +import { AuthorizationDataService } from '@dspace/core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '@dspace/core/data/feature-authorization/feature-id'; +import { RemoteData } from '@dspace/core/data/remote-data'; +import { ConfigurationProperty } from '@dspace/core/shared/configuration-property.model'; +import { DSpaceObject } from '@dspace/core/shared/dspace-object.model'; +import { getFirstCompletedRemoteData } from '@dspace/core/shared/operators'; import { combineLatest, map, Observable, } from 'rxjs'; -import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; -import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; -import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; -import { RemoteData } from '../../../core/data/remote-data'; -import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; -import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { LinkMenuItemModel } from '../menu-item/models/link.model'; import { MenuItemType } from '../menu-item-type.model'; import { PartialMenuSection } from '../menu-provider.model'; diff --git a/src/app/shared/menu/providers/audit-overview.menu.spec.ts b/src/app/shared/menu/providers/audit-overview.menu.spec.ts index e0d4b2e1fe1..9ea3b5cbabc 100644 --- a/src/app/shared/menu/providers/audit-overview.menu.spec.ts +++ b/src/app/shared/menu/providers/audit-overview.menu.spec.ts @@ -7,12 +7,12 @@ */ import { TestBed } from '@angular/core/testing'; +import { APP_CONFIG } from '@dspace/config/app-config.interface'; +import { AuthorizationDataService } from '@dspace/core/data/feature-authorization/authorization-data.service'; +import { AuthorizationDataServiceStub } from '@dspace/core/testing/authorization-service.stub'; import { of } from 'rxjs'; -import { APP_CONFIG } from '../../../../config/app-config.interface'; import { environment } from '../../../../environments/environment'; -import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; -import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub'; import { MenuItemType } from '../menu-item-type.model'; import { PartialMenuSection } from '../menu-provider.model'; import { AuditOverviewMenuProvider } from './audit-overview.menu'; diff --git a/src/app/shared/menu/providers/audit-overview.menu.ts b/src/app/shared/menu/providers/audit-overview.menu.ts index 2e64748beb4..34b49d89ccb 100644 --- a/src/app/shared/menu/providers/audit-overview.menu.ts +++ b/src/app/shared/menu/providers/audit-overview.menu.ts @@ -10,18 +10,18 @@ import { Inject, Injectable, } from '@angular/core'; +import { + APP_CONFIG, + AppConfig, +} from '@dspace/config/app-config.interface'; +import { AuthorizationDataService } from '@dspace/core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '@dspace/core/data/feature-authorization/feature-id'; import { combineLatest, map, Observable, } from 'rxjs'; -import { - APP_CONFIG, - AppConfig, -} from '../../../../config/app-config.interface'; -import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; -import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { MenuItemType } from '../menu-item-type.model'; import { AbstractMenuProvider, diff --git a/src/app/shared/utils/string-replace.pipe.ts b/src/app/shared/utils/string-replace.pipe.ts index 2e7eeb6a257..9030459a90f 100644 --- a/src/app/shared/utils/string-replace.pipe.ts +++ b/src/app/shared/utils/string-replace.pipe.ts @@ -2,8 +2,8 @@ import { Pipe, PipeTransform, } from '@angular/core'; +import { hasValue } from '@dspace/shared/utils/empty.util'; -import { hasValue } from '../empty.util'; @Pipe({ name: 'dsStringReplace', From 8c5bb1d7ff9907e0adc72188bf2047b7f26813a3 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Mon, 17 Nov 2025 11:35:30 +0100 Subject: [PATCH 16/22] [DURACOM-317] refactor, improve e2e, adapt route structure --- config/config.example.yml | 4 --- cypress/e2e/audit-overview-page.cy.ts | 15 ++++++++- src/app/audit-page/audit-page-routes.ts | 9 ------ .../audit-table/audit-table.component.html | 4 +-- ....html => object-audit-logs.component.html} | 11 +++++-- ...ts => object-audit-logs.component.spec.ts} | 14 ++++---- ...nent.ts => object-audit-logs.component.ts} | 8 ++--- .../overview/audit-overview.component.html | 5 +-- src/app/item-page/item-page-routes.ts | 11 +++++++ src/app/item-page/item-page-routing-paths.ts | 1 + .../menu/providers/audit-item.menu.spec.ts | 4 +-- .../shared/menu/providers/audit-item.menu.ts | 10 +++--- .../providers/audit-overview.menu.spec.ts | 14 +++++++- .../menu/providers/audit-overview.menu.ts | 17 ++++++++-- src/assets/i18n/en.json5 | 32 ++++--------------- src/config/app-config.interface.ts | 1 - src/config/default-app-config.ts | 2 -- src/environments/environment.test.ts | 2 -- 18 files changed, 92 insertions(+), 72 deletions(-) rename src/app/audit-page/object-audit-overview/{object-audit-overview.component.html => object-audit-logs.component.html} (60%) rename src/app/audit-page/object-audit-overview/{object-audit-overview.component.spec.ts => object-audit-logs.component.spec.ts} (91%) rename src/app/audit-page/object-audit-overview/{object-audit-overview.component.ts => object-audit-logs.component.ts} (95%) diff --git a/config/config.example.yml b/config/config.example.yml index 9008dfb1b70..0692d29afc1 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -626,7 +626,3 @@ geospatialMapViewer: accessibility: # The duration in days after which the accessibility settings cookie expires cookieExpirationDuration: 7 - -# Option to enable the menu entry to see audit logs on a site level. -# Only site administrators will be able to use this functionality -enableAuditLogsOverview: true diff --git a/cypress/e2e/audit-overview-page.cy.ts b/cypress/e2e/audit-overview-page.cy.ts index ab0f2b41f96..1e884182c50 100644 --- a/cypress/e2e/audit-overview-page.cy.ts +++ b/cypress/e2e/audit-overview-page.cy.ts @@ -7,10 +7,23 @@ describe('Audit Overview Page', () => { cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); }); - it('should pass accessibility tests', () => { + it('page structure should be correct and should pass accessibility tests', () => { // Page must first be visible cy.get('ds-audit-overview').should('be.visible'); + // Check for presence of main container and title + cy.get('.container').should('exist'); + cy.get('[data-test="audit-title"]').should('be.visible'); + cy.get('body').then($body => { + const hasTable = $body.find('[data-test="audit-table"]').length > 0; + const hasEmpty = $body.find('[data-test="audit-empty"]').length > 0; + // At least one present and not both + expect(hasTable || hasEmpty).to.equal(true); + expect(!(hasTable && hasEmpty)).to.equal(true); + }); // Analyze for accessibility issues testA11y('ds-audit-overview'); }); + + + }); diff --git a/src/app/audit-page/audit-page-routes.ts b/src/app/audit-page/audit-page-routes.ts index 4f5a85f6b99..541aa78cc55 100644 --- a/src/app/audit-page/audit-page-routes.ts +++ b/src/app/audit-page/audit-page-routes.ts @@ -2,7 +2,6 @@ import { Route } from '@angular/router'; import { authenticatedGuard } from '../core/auth/authenticated.guard'; import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { ObjectAuditOverviewComponent } from './object-audit-overview/object-audit-overview.component'; import { AuditOverviewComponent } from './overview/audit-overview.component'; export const ROUTES: Route[] = [ @@ -16,14 +15,6 @@ export const ROUTES: Route[] = [ data: { title: 'audit.overview.title', breadcrumbKey: 'audit.overview' }, resolve: { breadcrumb: i18nBreadcrumbResolver }, }, - { - path: 'object/:objectId', - component: ObjectAuditOverviewComponent, - data: { title: 'audit.object.title', breadcrumbKey: 'audit.object' }, - resolve: { - breadcrumb: i18nBreadcrumbResolver, - }, - }, ], }, diff --git a/src/app/audit-page/audit-table/audit-table.component.html b/src/app/audit-page/audit-table/audit-table.component.html index 4ed0b609663..86604ed8161 100644 --- a/src/app/audit-page/audit-table/audit-table.component.html +++ b/src/app/audit-page/audit-table/audit-table.component.html @@ -51,13 +51,13 @@ @if (isOverviewPage) { @if (audit.objectUUID) { - {{audit.objectUUID}} + {{audit.objectUUID}} } {{ audit.objectType }} @if (audit.subjectUUID) { - {{audit.subjectUUID}} + {{audit.subjectUUID}} } {{ audit.subjectType }} diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html b/src/app/audit-page/object-audit-overview/object-audit-logs.component.html similarity index 60% rename from src/app/audit-page/object-audit-overview/object-audit-overview.component.html rename to src/app/audit-page/object-audit-overview/object-audit-logs.component.html index ac42f242c64..1d29477a884 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html +++ b/src/app/audit-page/object-audit-overview/object-audit-logs.component.html @@ -5,7 +5,10 @@

{{'audit.object.overview.title' | translate}}

@if (objectId$ | async) {
- {{ objectName }} + + {{ 'audit.object.logs.label' | translate}} + {{objectName}} +
@if ((auditsRD$ | async)?.payload; as audits) { {{'audit.object.overview.title' | translate}} [pageConfig]="pageConfig" [object]="object" > + } @else { +
{{ 'audit.data.not-found' | translate }}
} - @if ((auditsRD$ | async)?.statusCode === 404) {

{{'audit.object.overview.disabled.message' | translate}}

diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts b/src/app/audit-page/object-audit-overview/object-audit-logs.component.spec.ts similarity index 91% rename from src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts rename to src/app/audit-page/object-audit-overview/object-audit-logs.component.spec.ts index 88a0b59a961..9171be3e02a 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.spec.ts +++ b/src/app/audit-page/object-audit-overview/object-audit-logs.component.spec.ts @@ -31,11 +31,11 @@ import { TranslateModule } from '@ngx-translate/core'; import { of } from 'rxjs'; import { AuditTableComponent } from '../audit-table/audit-table.component'; -import { ObjectAuditOverviewComponent } from './object-audit-overview.component'; +import { ObjectAuditLogsComponent } from './object-audit-logs.component'; -describe('ObjectAuditOverviewComponent', () => { - let component: ObjectAuditOverviewComponent; - let fixture: ComponentFixture; +describe('ObjectAuditLogsComponent', () => { + let component: ObjectAuditLogsComponent; + let fixture: ComponentFixture; let auditService: AuditDataService; let audits: Audit[]; @@ -75,7 +75,7 @@ describe('ObjectAuditOverviewComponent', () => { TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), AuditTableComponent, - ObjectAuditOverviewComponent, + ObjectAuditLogsComponent, RouterLink, ], providers: [ @@ -91,7 +91,7 @@ describe('ObjectAuditOverviewComponent', () => { ], schemas: [NO_ERRORS_SCHEMA], }) - .overrideComponent(ObjectAuditOverviewComponent, { + .overrideComponent(ObjectAuditLogsComponent, { remove: { imports: [AuditTableComponent], }, @@ -100,7 +100,7 @@ describe('ObjectAuditOverviewComponent', () => { })); beforeEach(() => { - fixture = TestBed.createComponent(ObjectAuditOverviewComponent); + fixture = TestBed.createComponent(ObjectAuditLogsComponent); component = fixture.componentInstance; spyOn(component, 'setAudits').and.callThrough(); fixture.detectChanges(); diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts b/src/app/audit-page/object-audit-overview/object-audit-logs.component.ts similarity index 95% rename from src/app/audit-page/object-audit-overview/object-audit-overview.component.ts rename to src/app/audit-page/object-audit-overview/object-audit-logs.component.ts index 19492e62213..f03ebc1613e 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.ts +++ b/src/app/audit-page/object-audit-overview/object-audit-logs.component.ts @@ -50,8 +50,8 @@ import { AuditTableComponent } from '../audit-table/audit-table.component'; * Component displaying a list of all audit about a object in a paginated table */ @Component({ - selector: 'ds-object-audit-overview', - templateUrl: './object-audit-overview.component.html', + selector: 'ds-object-audit-logs', + templateUrl: './object-audit-logs.component.html', imports: [ AsyncPipe, AuditTableComponent, @@ -60,7 +60,7 @@ import { AuditTableComponent } from '../audit-table/audit-table.component'; ], standalone: true, }) -export class ObjectAuditOverviewComponent implements OnInit { +export class ObjectAuditLogsComponent implements OnInit { /** * The object extracted from the route. @@ -118,7 +118,7 @@ export class ObjectAuditOverviewComponent implements OnInit { ngOnInit(): void { this.objectId$ = this.route.paramMap.pipe( - map((paramMap: ParamMap) => paramMap.get('objectId')), + map((paramMap: ParamMap) => paramMap.get('id')), switchMap((id: string) => this.dSpaceObjectDataService.findById(id, true, true)), getFirstSucceededRemoteDataPayload(), tap((object) => { diff --git a/src/app/audit-page/overview/audit-overview.component.html b/src/app/audit-page/overview/audit-overview.component.html index c7ba6b70b3b..a5290ad6a2b 100644 --- a/src/app/audit-page/overview/audit-overview.component.html +++ b/src/app/audit-page/overview/audit-overview.component.html @@ -1,16 +1,17 @@
-

{{'audit.overview.title' | translate}}

+

{{'audit.overview.title' | translate}}

@if ((auditsRD$ | async)?.payload; as audits) { } @else { -
{{ 'audit.data.not-found' | translate }}
+
{{ 'audit.data.not-found' | translate }}
}
diff --git a/src/app/item-page/item-page-routes.ts b/src/app/item-page/item-page-routes.ts index bb3fb9ad1a8..09a45618962 100644 --- a/src/app/item-page/item-page-routes.ts +++ b/src/app/item-page/item-page-routes.ts @@ -1,9 +1,11 @@ import { Route } from '@angular/router'; import { accessTokenResolver } from '@dspace/core/auth/access-token.resolver'; import { authenticatedGuard } from '@dspace/core/auth/authenticated.guard'; +import { i18nBreadcrumbResolver } from '@dspace/core/breadcrumbs/i18n-breadcrumb.resolver'; import { itemBreadcrumbResolver } from '@dspace/core/breadcrumbs/item-breadcrumb.resolver'; import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths'; +import { ObjectAuditLogsComponent } from '../audit-page/object-audit-overview/object-audit-logs.component'; import { MenuRoute } from '../shared/menu/menu-route.model'; import { viewTrackerResolver } from '../statistics/angulartics/dspace/view-tracker.resolver'; import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/bitstream-request-a-copy-page.component'; @@ -12,6 +14,7 @@ import { ThemedFullItemPageComponent } from './full/themed-full-item-page.compon import { itemPageResolver } from './item-page.resolver'; import { ITEM_ACCESS_BY_TOKEN_PATH, + ITEM_AUDIT_LOGS_PATH, ITEM_EDIT_PATH, ORCID_PATH, UPLOAD_BITSTREAM_PATH, @@ -53,6 +56,14 @@ export const ROUTES: Route[] = [ tracking: viewTrackerResolver, }, }, + { + path: ITEM_AUDIT_LOGS_PATH, + component: ObjectAuditLogsComponent, + data: { title: 'audit.object.title', breadcrumbKey: 'audit.object' }, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + }, { path: ITEM_EDIT_PATH, loadChildren: () => import('./edit-item-page/edit-item-page-routes') diff --git a/src/app/item-page/item-page-routing-paths.ts b/src/app/item-page/item-page-routing-paths.ts index f6b731d813d..a6e8ca98d64 100644 --- a/src/app/item-page/item-page-routing-paths.ts +++ b/src/app/item-page/item-page-routing-paths.ts @@ -26,6 +26,7 @@ export function getItemVersionRoute(versionId: string) { return new URLCombiner(getItemModuleRoute(), ITEM_VERSION_PATH, versionId).toString(); } +export const ITEM_AUDIT_LOGS_PATH = 'auditlogs'; export const ITEM_EDIT_PATH = 'edit'; export const ITEM_EDIT_VERSIONHISTORY_PATH = 'versionhistory'; export const ITEM_VERSION_PATH = 'version'; diff --git a/src/app/shared/menu/providers/audit-item.menu.spec.ts b/src/app/shared/menu/providers/audit-item.menu.spec.ts index a86a33c32f8..7754c96b18b 100644 --- a/src/app/shared/menu/providers/audit-item.menu.spec.ts +++ b/src/app/shared/menu/providers/audit-item.menu.spec.ts @@ -22,9 +22,9 @@ describe('AuditLogsMenuProvider', () => { model: { type: MenuItemType.LINK, text: 'context-menu.actions.audit-item.btn', - link: new URLCombiner('/auditlogs/object/test-uuid').toString(), + link: new URLCombiner('/collections/test-uuid/auditlogs').toString(), }, - icon: 'key', + icon: 'clipboard-check', }, ]; diff --git a/src/app/shared/menu/providers/audit-item.menu.ts b/src/app/shared/menu/providers/audit-item.menu.ts index 590a73e9083..d9b94b7de15 100644 --- a/src/app/shared/menu/providers/audit-item.menu.ts +++ b/src/app/shared/menu/providers/audit-item.menu.ts @@ -10,9 +10,11 @@ import { ConfigurationDataService } from '@dspace/core/data/configuration-data.s import { AuthorizationDataService } from '@dspace/core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '@dspace/core/data/feature-authorization/feature-id'; import { RemoteData } from '@dspace/core/data/remote-data'; +import { getDSORoute } from '@dspace/core/router/utils/dso-route.utils'; import { ConfigurationProperty } from '@dspace/core/shared/configuration-property.model'; import { DSpaceObject } from '@dspace/core/shared/dspace-object.model'; import { getFirstCompletedRemoteData } from '@dspace/core/shared/operators'; +import { URLCombiner } from '@dspace/core/url-combiner/url-combiner'; import { combineLatest, map, @@ -39,7 +41,7 @@ export class AuditLogsMenuProvider extends DSpaceObjectPageMenuProvider { public getSectionsForContext(dso: DSpaceObject): Observable { return combineLatest([ this.authorizationDataService.isAuthorized(FeatureID.AdministratorOf), - this.configurationDataService.findByPropertyName('context-menu-entry.audit.enabled').pipe( + this.configurationDataService.findByPropertyName('audit.context-menu-entry.enabled').pipe( getFirstCompletedRemoteData(), map((response: RemoteData) => { return response.hasSucceeded ? (response.payload.values.length > 0 && response.payload.values[0] === 'true') : false; @@ -51,10 +53,10 @@ export class AuditLogsMenuProvider extends DSpaceObjectPageMenuProvider { model: { type: MenuItemType.LINK, text: 'context-menu.actions.audit-item.btn', - link: '/auditlogs/object/' + dso.uuid, + link: new URLCombiner(getDSORoute(dso), 'auditlogs').toString(), } as LinkMenuItemModel, - icon: 'key', - visible: isAuditEnabled && isAdmin, + icon: 'clipboard-check', + visible: isAdmin && isAuditEnabled, }] as PartialMenuSection[]; }), ); diff --git a/src/app/shared/menu/providers/audit-overview.menu.spec.ts b/src/app/shared/menu/providers/audit-overview.menu.spec.ts index 9ea3b5cbabc..b622ed82544 100644 --- a/src/app/shared/menu/providers/audit-overview.menu.spec.ts +++ b/src/app/shared/menu/providers/audit-overview.menu.spec.ts @@ -8,8 +8,11 @@ import { TestBed } from '@angular/core/testing'; import { APP_CONFIG } from '@dspace/config/app-config.interface'; +import { ConfigurationDataService } from '@dspace/core/data/configuration-data.service'; import { AuthorizationDataService } from '@dspace/core/data/feature-authorization/authorization-data.service'; +import { ConfigurationProperty } from '@dspace/core/shared/configuration-property.model'; import { AuthorizationDataServiceStub } from '@dspace/core/testing/authorization-service.stub'; +import { createSuccessfulRemoteDataObject$ } from '@dspace/core/utilities/remote-data.utils'; import { of } from 'rxjs'; import { environment } from '../../../../environments/environment'; @@ -26,12 +29,20 @@ describe('AuditOverviewMenuProvider', () => { text: 'menu.section.audit', link: '/auditlogs', }, - icon: 'key', + icon: 'clipboard-check', }, ]; let provider: AuditOverviewMenuProvider; let authorizationServiceStub = new AuthorizationDataServiceStub(); + const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'audit.enabled', + values: [ + 'true', + ], + })), + }); beforeEach(() => { spyOn(authorizationServiceStub, 'isAuthorized').and.returnValue( @@ -42,6 +53,7 @@ describe('AuditOverviewMenuProvider', () => { providers: [ AuditOverviewMenuProvider, { provide: AuthorizationDataService, useValue: authorizationServiceStub }, + { provide: ConfigurationDataService, useValue: configurationDataService }, { provide: APP_CONFIG, useValue: environment }, ], }); diff --git a/src/app/shared/menu/providers/audit-overview.menu.ts b/src/app/shared/menu/providers/audit-overview.menu.ts index 34b49d89ccb..59db0297e16 100644 --- a/src/app/shared/menu/providers/audit-overview.menu.ts +++ b/src/app/shared/menu/providers/audit-overview.menu.ts @@ -14,8 +14,12 @@ import { APP_CONFIG, AppConfig, } from '@dspace/config/app-config.interface'; +import { ConfigurationDataService } from '@dspace/core/data/configuration-data.service'; import { AuthorizationDataService } from '@dspace/core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '@dspace/core/data/feature-authorization/feature-id'; +import { RemoteData } from '@dspace/core/data/remote-data'; +import { ConfigurationProperty } from '@dspace/core/shared/configuration-property.model'; +import { getFirstCompletedRemoteData } from '@dspace/core/shared/operators'; import { combineLatest, map, @@ -35,6 +39,7 @@ import { export class AuditOverviewMenuProvider extends AbstractMenuProvider { constructor( protected authorizationService: AuthorizationDataService, + protected configurationDataService: ConfigurationDataService, @Inject(APP_CONFIG) protected appConfig: AppConfig, ) { super(); @@ -43,17 +48,23 @@ export class AuditOverviewMenuProvider extends AbstractMenuProvider { public getSections(): Observable { return combineLatest([ this.authorizationService.isAuthorized(FeatureID.AdministratorOf), + this.configurationDataService.findByPropertyName('audit.enabled').pipe( + getFirstCompletedRemoteData(), + map((response: RemoteData) => { + return response.hasSucceeded ? (response.payload.values.length > 0 && response.payload.values[0] === 'true') : false; + }), + ), ]).pipe( - map(([isSiteAdmin]) => { + map(([isSiteAdmin, isAuditEnabled]) => { return [ { - visible: isSiteAdmin && this.appConfig.enableAuditLogsOverview, + visible: isSiteAdmin && isAuditEnabled, model: { type: MenuItemType.LINK, text: 'menu.section.audit', link: '/auditlogs', }, - icon: 'key', + icon: 'clipboard-check', }, ] as PartialMenuSection[]; }), diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 052a3e8b327..60a3d0bf13d 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -901,26 +901,6 @@ "audit.data.not-found": "No audits found.", - "audit.detail.breadcrumbs": "Audit Details", - - "audit.detail.title": "Audit Detail", - - "audit.detail.id": "Audit Id", - - "audit.detail.subjectUUID": "Subject ID", - - "audit.detail.subjectType": "Subject Type", - - "audit.detail.eventType": "Audit Type", - - "audit.detail.eperson": "EPerson", - - "audit.detail.timeStamp": "Time", - - "audit.detail.back": "All Audit Logs", - - "audit.detail.back.subject": "Subject Audit Logs", - "audit.detail.metadata.field": "Metadata field:", "audit.detail.metadata.value": "Value:", @@ -935,7 +915,7 @@ "audit.detail.metadata.checksum": "Checksum:", - "audit.overview.title": "Audit Logs Overview", + "audit.overview.title": "All Audit Logs", "audit.overview.table.id": "Audit ID", @@ -955,13 +935,15 @@ "audit.overview.table.timestamp": "Time", - "audit.overview.breadcrumbs": "Audit Logs Overview", + "audit.overview.breadcrumbs": "Audit Logs", "audit.object.back": "Back", - "audit.object.breadcrumbs": "Subject Audit Logs", + "audit.object.breadcrumbs": "Object Audit Logs", + + "audit.object.overview.title": "Object Audit Logs", - "audit.object.overview.title": "Subject Audit Logs Overview", + "audit.object.logs.label": "Logs for object: ", "audit.object.overview.disabled.message": "Audit feature is currently disabled", @@ -3507,7 +3489,7 @@ "menu.section.access_control_people": "People", - "menu.section.audit": "Audit Overview", + "menu.section.audit": "Audit Logs", "menu.section.reports": "Reports", diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 184e17332b4..d7904ea0773 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -69,7 +69,6 @@ interface AppConfig extends Config { matomo?: MatomoConfig; geospatialMapViewer: GeospatialMapConfig; accessibility: AccessibilitySettingsConfig; - enableAuditLogsOverview?: boolean; } /** diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 26e4d3bf1c2..f3692a3fa7c 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -641,6 +641,4 @@ export class DefaultAppConfig implements AppConfig { accessibility: AccessibilitySettingsConfig = { cookieExpirationDuration: 7, }; - - enableAuditLogsOverview = true; } diff --git a/src/environments/environment.test.ts b/src/environments/environment.test.ts index 4aff860b16b..eb9f754c8e6 100644 --- a/src/environments/environment.test.ts +++ b/src/environments/environment.test.ts @@ -476,6 +476,4 @@ export const environment: BuildConfig = { accessibility: { cookieExpirationDuration: 7, }, - - enableAuditLogsOverview: true, }; From 39f8864a262837e01b655467abcab07b9e9c9701 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Mon, 17 Nov 2025 11:53:14 +0100 Subject: [PATCH 17/22] [DURACOM-317] fix comm/coll routes for audit --- .../audit-page/audit-table/audit-table.component.html | 4 ++-- .../audit-page/audit-table/audit-table.component.ts | 11 +++++++++++ src/app/collection-page/collection-page-routes.ts | 9 +++++++++ src/app/community-page/community-page-routes.ts | 9 +++++++++ src/app/shared/menu/providers/audit-item.menu.ts | 2 +- 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/app/audit-page/audit-table/audit-table.component.html b/src/app/audit-page/audit-table/audit-table.component.html index 86604ed8161..78982f577db 100644 --- a/src/app/audit-page/audit-table/audit-table.component.html +++ b/src/app/audit-page/audit-table/audit-table.component.html @@ -51,13 +51,13 @@ @if (isOverviewPage) { @if (audit.objectUUID) { - {{audit.objectUUID}} + {{audit.objectUUID}} } {{ audit.objectType }} @if (audit.subjectUUID) { - {{audit.subjectUUID}} + {{audit.subjectUUID}} } {{ audit.subjectType }} diff --git a/src/app/audit-page/audit-table/audit-table.component.ts b/src/app/audit-page/audit-table/audit-table.component.ts index 8768230ea5b..7973d815ec5 100644 --- a/src/app/audit-page/audit-table/audit-table.component.ts +++ b/src/app/audit-page/audit-table/audit-table.component.ts @@ -11,9 +11,14 @@ import { } from '@angular/core'; import { RouterLink } from '@angular/router'; import { AUDIT_PERSON_NOT_AVAILABLE } from '@dspace/core/data/audit-data.service'; +import { RemoteData } from '@dspace/core/data/remote-data'; import { PaginationComponentOptions } from '@dspace/core/pagination/pagination-component-options.model'; +import { getDSORoute } from '@dspace/core/router/utils/dso-route.utils'; +import { URLCombiner } from '@dspace/core/url-combiner/url-combiner'; import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; import { Audit } from '../../core/audit/model/audit.model'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; @@ -78,4 +83,10 @@ export class AuditTableComponent { audit.isCollapsed = !audit.isCollapsed; this.changeDetectorRef.detectChanges(); } + + getObjectRoute$(dso: Observable>): Observable { + return dso.pipe( + map(resolvedDso => new URLCombiner(getDSORoute(resolvedDso.payload), 'auditlogs').toString()), + ); + } } diff --git a/src/app/collection-page/collection-page-routes.ts b/src/app/collection-page/collection-page-routes.ts index 2dd6f5efe8b..217b222fb83 100644 --- a/src/app/collection-page/collection-page-routes.ts +++ b/src/app/collection-page/collection-page-routes.ts @@ -4,6 +4,7 @@ import { collectionBreadcrumbResolver } from '@dspace/core/breadcrumbs/collectio import { communityBreadcrumbResolver } from '@dspace/core/breadcrumbs/community-breadcrumb.resolver'; import { i18nBreadcrumbResolver } from '@dspace/core/breadcrumbs/i18n-breadcrumb.resolver'; +import { ObjectAuditLogsComponent } from '../audit-page/object-audit-overview/object-audit-logs.component'; import { browseByGuard } from '../browse-by/browse-by-guard'; import { browseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; @@ -62,6 +63,14 @@ export const ROUTES: Route[] = [ .then((m) => m.ROUTES), canActivate: [collectionPageAdministratorGuard], }, + { + path: 'auditlogs', + component: ObjectAuditLogsComponent, + data: { title: 'audit.object.title', breadcrumbKey: 'audit.object' }, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + }, { path: 'delete', pathMatch: 'full', diff --git a/src/app/community-page/community-page-routes.ts b/src/app/community-page/community-page-routes.ts index 791bfa6f9b0..d944194ed72 100644 --- a/src/app/community-page/community-page-routes.ts +++ b/src/app/community-page/community-page-routes.ts @@ -3,6 +3,7 @@ import { authenticatedGuard } from '@dspace/core/auth/authenticated.guard'; import { communityBreadcrumbResolver } from '@dspace/core/breadcrumbs/community-breadcrumb.resolver'; import { i18nBreadcrumbResolver } from '@dspace/core/breadcrumbs/i18n-breadcrumb.resolver'; +import { ObjectAuditLogsComponent } from '../audit-page/object-audit-overview/object-audit-logs.component'; import { browseByGuard } from '../browse-by/browse-by-guard'; import { browseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; @@ -65,6 +66,14 @@ export const ROUTES: Route[] = [ component: DeleteCommunityPageComponent, canActivate: [authenticatedGuard], }, + { + path: 'auditlogs', + component: ObjectAuditLogsComponent, + data: { title: 'audit.object.title', breadcrumbKey: 'audit.object' }, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + }, { path: '', component: ThemedCommunityPageComponent, diff --git a/src/app/shared/menu/providers/audit-item.menu.ts b/src/app/shared/menu/providers/audit-item.menu.ts index d9b94b7de15..bcfc06f3659 100644 --- a/src/app/shared/menu/providers/audit-item.menu.ts +++ b/src/app/shared/menu/providers/audit-item.menu.ts @@ -56,7 +56,7 @@ export class AuditLogsMenuProvider extends DSpaceObjectPageMenuProvider { link: new URLCombiner(getDSORoute(dso), 'auditlogs').toString(), } as LinkMenuItemModel, icon: 'clipboard-check', - visible: isAdmin && isAuditEnabled, + visible: true, }] as PartialMenuSection[]; }), ); From b20a4d6e654e7689fa9c4e9e7b117e57818682c3 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Thu, 20 Nov 2025 17:21:11 +0100 Subject: [PATCH 18/22] [DURACOM-317] fix mock --- src/app/core/testing/audit.mock.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/core/testing/audit.mock.ts b/src/app/core/testing/audit.mock.ts index 08a379bb557..e3d99fa73a3 100644 --- a/src/app/core/testing/audit.mock.ts +++ b/src/app/core/testing/audit.mock.ts @@ -1,5 +1,7 @@ import { Audit } from '@dspace/core/audit/model/audit.model'; import { EPerson } from '@dspace/core/eperson/models/eperson.model'; +import { DSpaceObject } from '@dspace/core/shared/dspace-object.model'; +import { createSuccessfulRemoteDataObject$ } from '@dspace/core/utilities/remote-data.utils'; export const AuditEPersonMock: EPerson = Object.assign(new EPerson(), { handle: null, @@ -54,8 +56,10 @@ export const AuditMock: Audit = Object.assign(new Audit(), { id: '6fcd7329-8439-4492-bb72-0a4240b52da8', objectType: 'ITEM', objectUUID: 'objectUUID', + object: createSuccessfulRemoteDataObject$(new DSpaceObject()), subjectType: 'ITEM', subjectUUID: '3a74fe2c-d353-4e33-9887-d50184662dd4', + subject: createSuccessfulRemoteDataObject$(new DSpaceObject()), timeStamp: '2020-11-13T10:41:06.223+0000', epersonName: AuditEPersonMock.name, type: 'auditevent', From 48f4662cdf1bf1b669178a75e4c43583ca9d5227 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Fri, 21 Nov 2025 11:22:58 +0100 Subject: [PATCH 19/22] [DURACOM-317] fix table links, fix tests --- .../audit-table/audit-table.component.html | 18 ++++++++++++--- .../audit-table/audit-table.component.spec.ts | 6 +++++ .../audit-table/audit-table.component.ts | 22 ++++++++++++++----- .../menu/providers/audit-item.menu.spec.ts | 2 +- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/app/audit-page/audit-table/audit-table.component.html b/src/app/audit-page/audit-table/audit-table.component.html index 78982f577db..cf9cd0d06e1 100644 --- a/src/app/audit-page/audit-table/audit-table.component.html +++ b/src/app/audit-page/audit-table/audit-table.component.html @@ -51,13 +51,25 @@ @if (isOverviewPage) { @if (audit.objectUUID) { - {{audit.objectUUID}} + + @if (objectRoute !== ('/' + auditPath)) { + {{audit.objectUUID}} + } @else { + {{audit.objectUUID}} + } + } {{ audit.objectType }} @if (audit.subjectUUID) { - {{audit.subjectUUID}} + + @if (subjectRoute !== ('/' + auditPath)) { + {{audit.subjectUUID}} + } @else { + {{audit.subjectUUID}} + } + } {{ audit.subjectType }} @@ -65,7 +77,7 @@ @if (audit.otherAuditObject; as dso) { - {{ dsoNameService.getName(dso) }} ({{ dso.type }}) + {{ getDsoName(dso) }} ({{ dso.type }}) } @else { {{ dataNotAvailable }} } diff --git a/src/app/audit-page/audit-table/audit-table.component.spec.ts b/src/app/audit-page/audit-table/audit-table.component.spec.ts index 3e1b8c54b2c..04515769ec8 100644 --- a/src/app/audit-page/audit-table/audit-table.component.spec.ts +++ b/src/app/audit-page/audit-table/audit-table.component.spec.ts @@ -8,9 +8,12 @@ import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; import { Audit } from '@dspace/core/audit/model/audit.model'; import { DSONameService } from '@dspace/core/breadcrumbs/dso-name.service'; +import { DSpaceObjectDataService } from '@dspace/core/data/dspace-object-data.service'; import { PaginatedList } from '@dspace/core/data/paginated-list.model'; +import { DSpaceObject } from '@dspace/core/shared/dspace-object.model'; import { AuditMock } from '@dspace/core/testing/audit.mock'; import { DSONameServiceMock } from '@dspace/core/testing/dso-name.service.mock'; +import { createSuccessfulRemoteDataObject$ } from '@dspace/core/utilities/remote-data.utils'; import { TranslateModule } from '@ngx-translate/core'; import { PaginationComponent } from 'src/app/shared/pagination/pagination.component'; @@ -21,6 +24,8 @@ describe('AuditTableComponent', () => { let fixture: ComponentFixture; let audits = new PaginatedList() as PaginatedList; + const dSpaceObjectDataService = jasmine.createSpyObj('DSpaceObjectDataService', { findById: createSuccessfulRemoteDataObject$(new DSpaceObject()) }); + beforeEach(waitForAsync(() => { audits.page = [ AuditMock ]; @@ -33,6 +38,7 @@ describe('AuditTableComponent', () => { ], providers: [ { provide: DSONameService, useValue: new DSONameServiceMock() }, + { provide: DSpaceObjectDataService, useValue: dSpaceObjectDataService }, ], schemas: [NO_ERRORS_SCHEMA], }) diff --git a/src/app/audit-page/audit-table/audit-table.component.ts b/src/app/audit-page/audit-table/audit-table.component.ts index 7973d815ec5..8e69f9fc2cc 100644 --- a/src/app/audit-page/audit-table/audit-table.component.ts +++ b/src/app/audit-page/audit-table/audit-table.component.ts @@ -11,9 +11,10 @@ import { } from '@angular/core'; import { RouterLink } from '@angular/router'; import { AUDIT_PERSON_NOT_AVAILABLE } from '@dspace/core/data/audit-data.service'; -import { RemoteData } from '@dspace/core/data/remote-data'; +import { DSpaceObjectDataService } from '@dspace/core/data/dspace-object-data.service'; import { PaginationComponentOptions } from '@dspace/core/pagination/pagination-component-options.model'; import { getDSORoute } from '@dspace/core/router/utils/dso-route.utils'; +import { getFirstSucceededRemoteDataPayload } from '@dspace/core/shared/operators'; import { URLCombiner } from '@dspace/core/url-combiner/url-combiner'; import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; @@ -66,6 +67,11 @@ export class AuditTableComponent { */ @Input() object: DSpaceObject; + /** + * Path for audit logs + */ + readonly auditPath = 'auditlogs'; + protected readonly dataNotAvailable = AUDIT_PERSON_NOT_AVAILABLE; /** @@ -74,8 +80,9 @@ export class AuditTableComponent { protected readonly dateFormat = 'yyyy-MM-dd HH:mm:ss'; constructor( - public dsoNameService: DSONameService, + private dsoNameService: DSONameService, private changeDetectorRef: ChangeDetectorRef, + private dsoDataService: DSpaceObjectDataService, ) {} @@ -84,9 +91,14 @@ export class AuditTableComponent { this.changeDetectorRef.detectChanges(); } - getObjectRoute$(dso: Observable>): Observable { - return dso.pipe( - map(resolvedDso => new URLCombiner(getDSORoute(resolvedDso.payload), 'auditlogs').toString()), + getObjectRoute$(id: string): Observable { + return this.dsoDataService.findById(id).pipe( + getFirstSucceededRemoteDataPayload(), + map(resolvedDso => new URLCombiner(getDSORoute(resolvedDso), this.auditPath).toString()), ); } + + getDsoName(dso: DSpaceObject): string { + return this.dsoNameService.getName(dso); + } } diff --git a/src/app/shared/menu/providers/audit-item.menu.spec.ts b/src/app/shared/menu/providers/audit-item.menu.spec.ts index 7754c96b18b..df9a9db961f 100644 --- a/src/app/shared/menu/providers/audit-item.menu.spec.ts +++ b/src/app/shared/menu/providers/audit-item.menu.spec.ts @@ -38,7 +38,7 @@ describe('AuditLogsMenuProvider', () => { const configurationDataService = jasmine.createSpyObj('configurationDataService', { findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { - name: 'context-menu-entry.audit.enabled', + name: 'audit.context-menu-entry.enabled', values: [ 'true', ], From f919f1d2a41c3438f641cf04ef9501784287e744 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Mon, 24 Nov 2025 16:18:06 +0100 Subject: [PATCH 20/22] [DURACOM-317] add description, adapt table header, fix menu visibility --- .../audit-table/audit-table.component.ts | 5 ++ .../shared/menu/providers/audit-item.menu.ts | 52 ++++++++++++------- src/assets/i18n/en.json5 | 2 +- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/app/audit-page/audit-table/audit-table.component.ts b/src/app/audit-page/audit-table/audit-table.component.ts index 8e69f9fc2cc..aae807212a7 100644 --- a/src/app/audit-page/audit-table/audit-table.component.ts +++ b/src/app/audit-page/audit-table/audit-table.component.ts @@ -29,6 +29,11 @@ import { PaginationComponent } from '../../shared/pagination/pagination.componen import { StringReplacePipe } from '../../shared/utils/string-replace.pipe'; import { VarDirective } from '../../shared/utils/var.directive'; +/** + * Renders a paginated table of audit records, either in overview mode for all the environemnt or tied to a specific DSpaceObject. + * Supports row expansion to show details. + */ + @Component({ selector: 'ds-audit-table', templateUrl: './audit-table.component.html', diff --git a/src/app/shared/menu/providers/audit-item.menu.ts b/src/app/shared/menu/providers/audit-item.menu.ts index bcfc06f3659..3e014e7ef9f 100644 --- a/src/app/shared/menu/providers/audit-item.menu.ts +++ b/src/app/shared/menu/providers/audit-item.menu.ts @@ -19,7 +19,9 @@ import { combineLatest, map, Observable, + of, } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; import { LinkMenuItemModel } from '../menu-item/models/link.model'; import { MenuItemType } from '../menu-item-type.model'; @@ -39,26 +41,38 @@ export class AuditLogsMenuProvider extends DSpaceObjectPageMenuProvider { } public getSectionsForContext(dso: DSpaceObject): Observable { - return combineLatest([ - this.authorizationDataService.isAuthorized(FeatureID.AdministratorOf), - this.configurationDataService.findByPropertyName('audit.context-menu-entry.enabled').pipe( - getFirstCompletedRemoteData(), - map((response: RemoteData) => { - return response.hasSucceeded ? (response.payload.values.length > 0 && response.payload.values[0] === 'true') : false; - }), - ), - ]).pipe( - map(([isAdmin, isAuditEnabled]: [boolean, boolean]) => { - return [{ - model: { - type: MenuItemType.LINK, - text: 'context-menu.actions.audit-item.btn', - link: new URLCombiner(getDSORoute(dso), 'auditlogs').toString(), - } as LinkMenuItemModel, - icon: 'clipboard-check', - visible: true, - }] as PartialMenuSection[]; + return this.configurationDataService.findByPropertyName('audit.enabled').pipe( + getFirstCompletedRemoteData(), + map((response: RemoteData) => this.isPropertyEnabled(response)), + switchMap((isAuditEnabled: boolean) => { + if (isAuditEnabled) { + return combineLatest([ + this.authorizationDataService.isAuthorized(FeatureID.AdministratorOf), + this.configurationDataService.findByPropertyName('audit.context-menu-entry.enabled').pipe( + getFirstCompletedRemoteData(), + map((response: RemoteData) => this.isPropertyEnabled(response)), + ), + ]).pipe( + map(([isAdmin, isAuditMenuEnabled]: [boolean, boolean]) => { + return [{ + model: { + type: MenuItemType.LINK, + text: 'context-menu.actions.audit-item.btn', + link: new URLCombiner(getDSORoute(dso), 'auditlogs').toString(), + } as LinkMenuItemModel, + icon: 'clipboard-check', + visible: isAdmin && isAuditMenuEnabled, + }] as PartialMenuSection[]; + }), + ); + } else { + return of([]); + } }), ); } + + private isPropertyEnabled(property: RemoteData): boolean { + return property.hasSucceeded ? (property.payload.values.length > 0 && property.payload.values[0] === 'true') : false; + } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 60a3d0bf13d..d39a824b799 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -933,7 +933,7 @@ "audit.overview.table.other": "Other Object", - "audit.overview.table.timestamp": "Time", + "audit.overview.table.timestamp": "Time (UTC)", "audit.overview.breadcrumbs": "Audit Logs", From cea9adbd03f140d7367db860d1a4d2ce4fce5b23 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Thu, 11 Dec 2025 17:29:29 +0100 Subject: [PATCH 21/22] [DURACOM-317] improve labels --- src/app/audit-page/audit-table/audit-table.component.html | 2 +- src/app/audit-page/audit-table/audit-table.component.ts | 2 -- .../object-audit-overview/object-audit-logs.component.ts | 7 +------ src/assets/i18n/en.json5 | 4 +++- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/app/audit-page/audit-table/audit-table.component.html b/src/app/audit-page/audit-table/audit-table.component.html index cf9cd0d06e1..d44a5131ad3 100644 --- a/src/app/audit-page/audit-table/audit-table.component.html +++ b/src/app/audit-page/audit-table/audit-table.component.html @@ -79,7 +79,7 @@ @if (audit.otherAuditObject; as dso) { {{ getDsoName(dso) }} ({{ dso.type }}) } @else { - {{ dataNotAvailable }} + {{ 'audit.data.self' | translate}} } diff --git a/src/app/audit-page/audit-table/audit-table.component.ts b/src/app/audit-page/audit-table/audit-table.component.ts index aae807212a7..891822bd0cc 100644 --- a/src/app/audit-page/audit-table/audit-table.component.ts +++ b/src/app/audit-page/audit-table/audit-table.component.ts @@ -10,7 +10,6 @@ import { Input, } from '@angular/core'; import { RouterLink } from '@angular/router'; -import { AUDIT_PERSON_NOT_AVAILABLE } from '@dspace/core/data/audit-data.service'; import { DSpaceObjectDataService } from '@dspace/core/data/dspace-object-data.service'; import { PaginationComponentOptions } from '@dspace/core/pagination/pagination-component-options.model'; import { getDSORoute } from '@dspace/core/router/utils/dso-route.utils'; @@ -77,7 +76,6 @@ export class AuditTableComponent { */ readonly auditPath = 'auditlogs'; - protected readonly dataNotAvailable = AUDIT_PERSON_NOT_AVAILABLE; /** * Date format to use for start and end time of audits diff --git a/src/app/audit-page/object-audit-overview/object-audit-logs.component.ts b/src/app/audit-page/object-audit-overview/object-audit-logs.component.ts index f03ebc1613e..f03e8aa8f9d 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-logs.component.ts +++ b/src/app/audit-page/object-audit-overview/object-audit-logs.component.ts @@ -12,10 +12,7 @@ import { Router, RouterLink, } from '@angular/router'; -import { - AUDIT_PERSON_NOT_AVAILABLE, - AuditDataService, -} from '@dspace/core/data/audit-data.service'; +import { AuditDataService } from '@dspace/core/data/audit-data.service'; import { PaginationComponentOptions } from '@dspace/core/pagination/pagination-component-options.model'; import { getDSORoute } from '@dspace/core/router/utils/dso-route.utils'; import { TranslateModule } from '@ngx-translate/core'; @@ -104,8 +101,6 @@ export class ObjectAuditLogsComponent implements OnInit { objectRoute: string; - dataNotAvailable = AUDIT_PERSON_NOT_AVAILABLE; - constructor(protected route: ActivatedRoute, protected router: Router, protected auditService: AuditDataService, diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index d39a824b799..80ba3584fa0 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -901,6 +901,8 @@ "audit.data.not-found": "No audits found.", + "audit.data.self": "self", + "audit.detail.metadata.field": "Metadata field:", "audit.detail.metadata.value": "Value:", @@ -931,7 +933,7 @@ "audit.overview.table.eperson": "EPerson", - "audit.overview.table.other": "Other Object", + "audit.overview.table.other": "Object", "audit.overview.table.timestamp": "Time (UTC)", From 2001e3d7c76f7c737ee52c3af4e64e072c7f9a49 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Thu, 11 Dec 2025 17:49:55 +0100 Subject: [PATCH 22/22] [DURACOM-317] fix lint --- src/app/audit-page/audit-table/audit-table.component.ts | 1 - .../object-audit-overview/object-audit-logs.component.ts | 1 - src/app/audit-page/overview/audit-overview.component.ts | 1 - 3 files changed, 3 deletions(-) diff --git a/src/app/audit-page/audit-table/audit-table.component.ts b/src/app/audit-page/audit-table/audit-table.component.ts index 891822bd0cc..4b548b73f5c 100644 --- a/src/app/audit-page/audit-table/audit-table.component.ts +++ b/src/app/audit-page/audit-table/audit-table.component.ts @@ -48,7 +48,6 @@ import { VarDirective } from '../../shared/utils/var.directive'; TranslateModule, VarDirective, ], - standalone: true, }) export class AuditTableComponent { /** diff --git a/src/app/audit-page/object-audit-overview/object-audit-logs.component.ts b/src/app/audit-page/object-audit-overview/object-audit-logs.component.ts index f03e8aa8f9d..c09ca7f4f18 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-logs.component.ts +++ b/src/app/audit-page/object-audit-overview/object-audit-logs.component.ts @@ -55,7 +55,6 @@ import { AuditTableComponent } from '../audit-table/audit-table.component'; RouterLink, TranslateModule, ], - standalone: true, }) export class ObjectAuditLogsComponent implements OnInit { diff --git a/src/app/audit-page/overview/audit-overview.component.ts b/src/app/audit-page/overview/audit-overview.component.ts index b400bd258fa..94c96d4db90 100644 --- a/src/app/audit-page/overview/audit-overview.component.ts +++ b/src/app/audit-page/overview/audit-overview.component.ts @@ -37,7 +37,6 @@ import { AuditTableComponent } from '../audit-table/audit-table.component'; AuditTableComponent, TranslateModule, ], - standalone: true, }) export class AuditOverviewComponent implements OnInit {