diff --git a/backend/process-document/src/main/java/com/ritense/processdocument/autoconfigure/ProcessDocumentAuditAutoConfiguration.java b/backend/process-document/src/main/java/com/ritense/processdocument/autoconfigure/ProcessDocumentAuditAutoConfiguration.java index a5c3e24288..efcbf64b98 100644 --- a/backend/process-document/src/main/java/com/ritense/processdocument/autoconfigure/ProcessDocumentAuditAutoConfiguration.java +++ b/backend/process-document/src/main/java/com/ritense/processdocument/autoconfigure/ProcessDocumentAuditAutoConfiguration.java @@ -20,9 +20,12 @@ import com.ritense.audit.service.AuditService; import com.ritense.authorization.AuthorizationService; import com.ritense.document.service.impl.JsonSchemaDocumentService; +import com.ritense.processdocument.service.DocumentAuditEventProvider; import com.ritense.processdocument.service.ProcessDocumentAuditService; +import com.ritense.processdocument.service.impl.DefaultDocumentAuditEventProvider; import com.ritense.processdocument.service.impl.OperatonProcessJsonSchemaDocumentAuditService; import com.ritense.processdocument.web.rest.ProcessDocumentAuditResource; +import java.util.List; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -32,17 +35,25 @@ @ConditionalOnClass(AuditSearchService.class) public class ProcessDocumentAuditAutoConfiguration { + @Bean + @ConditionalOnMissingBean(DefaultDocumentAuditEventProvider.class) + public DefaultDocumentAuditEventProvider defaultDocumentAuditEventProvider() { + return new DefaultDocumentAuditEventProvider(); + } + @Bean @ConditionalOnMissingBean(ProcessDocumentAuditService.class) public OperatonProcessJsonSchemaDocumentAuditService processDocumentAuditService( AuditService auditService, JsonSchemaDocumentService documentService, - AuthorizationService authorizationService + AuthorizationService authorizationService, + List auditEventProviders ) { return new OperatonProcessJsonSchemaDocumentAuditService( auditService, documentService, - authorizationService + authorizationService, + auditEventProviders ); } diff --git a/backend/process-document/src/main/java/com/ritense/processdocument/service/impl/OperatonProcessJsonSchemaDocumentAuditService.java b/backend/process-document/src/main/java/com/ritense/processdocument/service/impl/OperatonProcessJsonSchemaDocumentAuditService.java index 2a3d9a7ef6..1de1f19bac 100644 --- a/backend/process-document/src/main/java/com/ritense/processdocument/service/impl/OperatonProcessJsonSchemaDocumentAuditService.java +++ b/backend/process-document/src/main/java/com/ritense/processdocument/service/impl/OperatonProcessJsonSchemaDocumentAuditService.java @@ -25,24 +25,10 @@ import com.ritense.authorization.request.EntityAuthorizationRequest; import com.ritense.document.domain.Document; import com.ritense.document.domain.impl.JsonSchemaDocument; -import com.ritense.document.domain.impl.event.JsonSchemaDocumentCreatedEvent; -import com.ritense.document.domain.impl.event.JsonSchemaDocumentModifiedEvent; -import com.ritense.document.event.DocumentAssigneeChangedEvent; -import com.ritense.document.event.DocumentRetentionPeriodSetEvent; -import com.ritense.document.event.DocumentRetentionPeriodUnsetEvent; -import com.ritense.document.event.DocumentUnassignedEvent; import com.ritense.document.service.impl.JsonSchemaDocumentService; -import com.ritense.processdocument.event.BesluitAddedEvent; +import com.ritense.processdocument.service.DocumentAuditEventProvider; import com.ritense.processdocument.service.ProcessDocumentAuditService; -import com.ritense.valtimo.operaton.processaudit.ProcessEndedEvent; -import com.ritense.valtimo.operaton.processaudit.ProcessStartedEvent; import com.ritense.valtimo.contract.audit.AuditEvent; -import com.ritense.valtimo.contract.document.event.DocumentRelatedFileAddedEvent; -import com.ritense.valtimo.contract.document.event.DocumentRelatedFileRemovedEvent; -import com.ritense.valtimo.contract.documentgeneration.event.DossierDocumentGeneratedEvent; -import com.ritense.valtimo.contract.event.TaskAssignedEvent; -import com.ritense.valtimo.contract.event.TaskCompletedEvent; -import com.ritense.valtimo.contract.event.TaskDueDateSetEvent; import java.util.List; import java.util.UUID; import org.springframework.data.domain.Page; @@ -52,17 +38,19 @@ public class OperatonProcessJsonSchemaDocumentAuditService implements ProcessDoc private final AuditService auditService; private final JsonSchemaDocumentService documentService; - private final AuthorizationService authorizationService; + private final List auditEventProviders; public OperatonProcessJsonSchemaDocumentAuditService( AuditService auditService, JsonSchemaDocumentService documentService, - AuthorizationService authorizationService + AuthorizationService authorizationService, + List auditEventProviders ) { this.auditService = auditService; this.documentService = documentService; this.authorizationService = authorizationService; + this.auditEventProviders = auditEventProviders; } @Override @@ -70,23 +58,9 @@ public Page getAuditLog( final Document.Id id, final Pageable pageable ) { - final List> eventTypes = List.of( - JsonSchemaDocumentCreatedEvent.class, - JsonSchemaDocumentModifiedEvent.class, - DocumentRetentionPeriodSetEvent.class, - DocumentRetentionPeriodUnsetEvent.class, - TaskAssignedEvent.class, - TaskCompletedEvent.class, - ProcessStartedEvent.class, - ProcessEndedEvent.class, - DossierDocumentGeneratedEvent.class, - DocumentRelatedFileAddedEvent.class, - DocumentRelatedFileRemovedEvent.class, - BesluitAddedEvent.class, - DocumentAssigneeChangedEvent.class, - DocumentUnassignedEvent.class, - TaskDueDateSetEvent.class - ); + final List> eventTypes = auditEventProviders.stream() + .flatMap(provider -> provider.getAuditEventTypes().stream()) + .toList(); final var document = documentService.getDocumentBy(id); authorizationService.requirePermission( diff --git a/backend/process-document/src/main/kotlin/com/ritense/processdocument/service/DocumentAuditEventProvider.kt b/backend/process-document/src/main/kotlin/com/ritense/processdocument/service/DocumentAuditEventProvider.kt new file mode 100644 index 0000000000..7b90276b57 --- /dev/null +++ b/backend/process-document/src/main/kotlin/com/ritense/processdocument/service/DocumentAuditEventProvider.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2015-2024 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ritense.processdocument.service + +import com.ritense.valtimo.contract.audit.AuditEvent + +fun interface DocumentAuditEventProvider { + fun getAuditEventTypes(): List> +} diff --git a/backend/process-document/src/main/kotlin/com/ritense/processdocument/service/impl/DefaultDocumentAuditEventProvider.kt b/backend/process-document/src/main/kotlin/com/ritense/processdocument/service/impl/DefaultDocumentAuditEventProvider.kt new file mode 100644 index 0000000000..5db5a8c745 --- /dev/null +++ b/backend/process-document/src/main/kotlin/com/ritense/processdocument/service/impl/DefaultDocumentAuditEventProvider.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2015-2024 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ritense.processdocument.service.impl + +import com.ritense.document.domain.impl.event.JsonSchemaDocumentCreatedEvent +import com.ritense.document.domain.impl.event.JsonSchemaDocumentModifiedEvent +import com.ritense.document.event.DocumentAssigneeChangedEvent +import com.ritense.document.event.DocumentRetentionPeriodSetEvent +import com.ritense.document.event.DocumentRetentionPeriodUnsetEvent +import com.ritense.document.event.DocumentUnassignedEvent +import com.ritense.processdocument.event.BesluitAddedEvent +import com.ritense.processdocument.service.DocumentAuditEventProvider +import com.ritense.valtimo.contract.audit.AuditEvent +import com.ritense.valtimo.contract.document.event.DocumentRelatedFileAddedEvent +import com.ritense.valtimo.contract.document.event.DocumentRelatedFileRemovedEvent +import com.ritense.valtimo.contract.documentgeneration.event.DossierDocumentGeneratedEvent +import com.ritense.valtimo.contract.event.TaskAssignedEvent +import com.ritense.valtimo.contract.event.TaskCompletedEvent +import com.ritense.valtimo.contract.event.TaskDueDateSetEvent +import com.ritense.valtimo.operaton.processaudit.ProcessEndedEvent +import com.ritense.valtimo.operaton.processaudit.ProcessStartedEvent + +class DefaultDocumentAuditEventProvider : DocumentAuditEventProvider { + override fun getAuditEventTypes(): List> = listOf( + JsonSchemaDocumentCreatedEvent::class.java, + JsonSchemaDocumentModifiedEvent::class.java, + DocumentRetentionPeriodSetEvent::class.java, + DocumentRetentionPeriodUnsetEvent::class.java, + TaskAssignedEvent::class.java, + TaskCompletedEvent::class.java, + ProcessStartedEvent::class.java, + ProcessEndedEvent::class.java, + DossierDocumentGeneratedEvent::class.java, + DocumentRelatedFileAddedEvent::class.java, + DocumentRelatedFileRemovedEvent::class.java, + BesluitAddedEvent::class.java, + DocumentAssigneeChangedEvent::class.java, + DocumentUnassignedEvent::class.java, + TaskDueDateSetEvent::class.java, + ) +} diff --git a/backend/process-document/src/test/kotlin/com/ritense/processdocument/service/impl/DefaultDocumentAuditEventProviderTest.kt b/backend/process-document/src/test/kotlin/com/ritense/processdocument/service/impl/DefaultDocumentAuditEventProviderTest.kt new file mode 100644 index 0000000000..ec0a1cc472 --- /dev/null +++ b/backend/process-document/src/test/kotlin/com/ritense/processdocument/service/impl/DefaultDocumentAuditEventProviderTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2015-2024 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ritense.processdocument.service.impl + +import com.ritense.document.domain.impl.event.JsonSchemaDocumentCreatedEvent +import com.ritense.document.domain.impl.event.JsonSchemaDocumentModifiedEvent +import com.ritense.document.event.DocumentAssigneeChangedEvent +import com.ritense.document.event.DocumentRetentionPeriodSetEvent +import com.ritense.document.event.DocumentRetentionPeriodUnsetEvent +import com.ritense.document.event.DocumentUnassignedEvent +import com.ritense.processdocument.event.BesluitAddedEvent +import com.ritense.valtimo.contract.document.event.DocumentRelatedFileAddedEvent +import com.ritense.valtimo.contract.document.event.DocumentRelatedFileRemovedEvent +import com.ritense.valtimo.contract.documentgeneration.event.DossierDocumentGeneratedEvent +import com.ritense.valtimo.contract.event.TaskAssignedEvent +import com.ritense.valtimo.contract.event.TaskCompletedEvent +import com.ritense.valtimo.contract.event.TaskDueDateSetEvent +import com.ritense.valtimo.operaton.processaudit.ProcessEndedEvent +import com.ritense.valtimo.operaton.processaudit.ProcessStartedEvent +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class DefaultDocumentAuditEventProviderTest { + + private val provider = DefaultDocumentAuditEventProvider() + + @Test + fun `should return exactly 15 event types`() { + assertThat(provider.getAuditEventTypes()).hasSize(15) + } + + @Test + fun `should return all expected event types`() { + val eventTypes = provider.getAuditEventTypes() + + assertThat(eventTypes).containsExactlyInAnyOrder( + JsonSchemaDocumentCreatedEvent::class.java, + JsonSchemaDocumentModifiedEvent::class.java, + DocumentRetentionPeriodSetEvent::class.java, + DocumentRetentionPeriodUnsetEvent::class.java, + TaskAssignedEvent::class.java, + TaskCompletedEvent::class.java, + ProcessStartedEvent::class.java, + ProcessEndedEvent::class.java, + DossierDocumentGeneratedEvent::class.java, + DocumentRelatedFileAddedEvent::class.java, + DocumentRelatedFileRemovedEvent::class.java, + BesluitAddedEvent::class.java, + DocumentAssigneeChangedEvent::class.java, + DocumentUnassignedEvent::class.java, + TaskDueDateSetEvent::class.java, + ) + } +} diff --git a/backend/process-document/src/test/kotlin/com/ritense/processdocument/service/impl/OperatonProcessJsonSchemaDocumentAuditServiceTest.kt b/backend/process-document/src/test/kotlin/com/ritense/processdocument/service/impl/OperatonProcessJsonSchemaDocumentAuditServiceTest.kt new file mode 100644 index 0000000000..5cbf4f7452 --- /dev/null +++ b/backend/process-document/src/test/kotlin/com/ritense/processdocument/service/impl/OperatonProcessJsonSchemaDocumentAuditServiceTest.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2015-2024 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ritense.processdocument.service.impl + +import com.ritense.audit.service.AuditService +import com.ritense.authorization.AuthorizationService +import com.ritense.authorization.request.AuthorizationRequest +import com.ritense.document.domain.impl.JsonSchemaDocument +import com.ritense.document.domain.impl.JsonSchemaDocumentId +import com.ritense.document.service.impl.JsonSchemaDocumentService +import com.ritense.processdocument.domain.event.TestEvent +import com.ritense.processdocument.service.DocumentAuditEventProvider +import com.ritense.valtimo.contract.audit.AuditEvent +import com.ritense.valtimo.contract.audit.AuditMetaData +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable +import java.time.LocalDateTime +import java.util.UUID + +internal class OperatonProcessJsonSchemaDocumentAuditServiceTest { + + private lateinit var auditService: AuditService + private lateinit var documentService: JsonSchemaDocumentService + private lateinit var authorizationService: AuthorizationService + private lateinit var document: JsonSchemaDocument + private lateinit var documentId: JsonSchemaDocumentId + + @BeforeEach + fun setUp() { + auditService = mock() + documentService = mock() + authorizationService = mock() + document = mock() + documentId = JsonSchemaDocumentId.existingId(UUID.randomUUID()) + + whenever(documentService.getDocumentBy(documentId)).thenReturn(document) + whenever(auditService.findByEventAndDocumentId( + org.mockito.kotlin.any>>(), + org.mockito.kotlin.any(), + org.mockito.kotlin.any(), + )).thenReturn(PageImpl(emptyList())) + } + + @Test + fun `getAuditLog should pass event types from provider to audit service`() { + val provider = DocumentAuditEventProvider { listOf(TestEvent::class.java) } + val service = buildService(listOf(provider)) + + service.getAuditLog(documentId, Pageable.unpaged()) + + val eventTypesCaptor = argumentCaptor>>() + verify(auditService).findByEventAndDocumentId( + eventTypesCaptor.capture(), + org.mockito.kotlin.any(), + org.mockito.kotlin.any(), + ) + assertThat(eventTypesCaptor.firstValue).containsExactly(TestEvent::class.java) + } + + @Test + fun `getAuditLog should merge event types from multiple providers`() { + val provider1 = DocumentAuditEventProvider { listOf(TestEvent::class.java) } + val provider2 = DocumentAuditEventProvider { listOf(CustomEvent::class.java) } + val service = buildService(listOf(provider1, provider2)) + + service.getAuditLog(documentId, Pageable.unpaged()) + + val eventTypesCaptor = argumentCaptor>>() + verify(auditService).findByEventAndDocumentId( + eventTypesCaptor.capture(), + org.mockito.kotlin.any(), + org.mockito.kotlin.any(), + ) + assertThat(eventTypesCaptor.firstValue).containsExactlyInAnyOrder( + TestEvent::class.java, + CustomEvent::class.java, + ) + } + + @Test + fun `getAuditLog with default provider should pass all 15 built-in event types`() { + val service = buildService(listOf(DefaultDocumentAuditEventProvider())) + + service.getAuditLog(documentId, Pageable.unpaged()) + + val eventTypesCaptor = argumentCaptor>>() + verify(auditService).findByEventAndDocumentId( + eventTypesCaptor.capture(), + org.mockito.kotlin.any(), + org.mockito.kotlin.any(), + ) + assertThat(eventTypesCaptor.firstValue).hasSize(15) + } + + @Test + fun `getAuditLog should pass the document UUID to audit service`() { + val service = buildService(listOf(DocumentAuditEventProvider { emptyList() })) + + service.getAuditLog(documentId, Pageable.unpaged()) + + val uuidCaptor = argumentCaptor() + verify(auditService).findByEventAndDocumentId( + org.mockito.kotlin.any>>(), + uuidCaptor.capture(), + org.mockito.kotlin.any(), + ) + assertThat(uuidCaptor.firstValue).isEqualTo(UUID.fromString(documentId.toString())) + } + + @Test + fun `getAuditLog should check authorization for the document`() { + val service = buildService(listOf(DocumentAuditEventProvider { emptyList() })) + + service.getAuditLog(documentId, Pageable.unpaged()) + + verify(authorizationService).requirePermission(org.mockito.kotlin.any>()) + } + + private fun buildService(providers: List) = + OperatonProcessJsonSchemaDocumentAuditService( + auditService, + documentService, + authorizationService, + providers, + ) + + /** Stub event class used to test extensibility without depending on specific built-in events. */ + private class CustomEvent( + id: UUID, + origin: String, + occurredOn: LocalDateTime, + user: String, + ) : AuditMetaData(id, origin, occurredOn, user), AuditEvent +} diff --git a/documentation/SUMMARY.md b/documentation/SUMMARY.md index d19f0e06fe..a282b15153 100644 --- a/documentation/SUMMARY.md +++ b/documentation/SUMMARY.md @@ -115,6 +115,7 @@ * [Custom case tabs](features/case/for-developers/case-tabs.md) * [Case migration](features/case/for-developers/case-migration.md) * [Custom case headers](features/case/for-developers/custom-case-headers.md) + * [Custom audit events](features/case/for-developers/custom-audit-events.md) * [Register Angular component](features/case/for-developers/register-angular-component.md) * [🧱 Building blocks](features/building-blocks/README.md) * [Forms](features/building-blocks/forms.md) diff --git a/documentation/features/case/for-developers/README.md b/documentation/features/case/for-developers/README.md index 7210b7e290..70a97178cc 100644 --- a/documentation/features/case/for-developers/README.md +++ b/documentation/features/case/for-developers/README.md @@ -33,3 +33,7 @@ The for developers section within each feature gives more tech heavy information {% content-ref url="custom-case-headers.md" %} [custom-case-headers.md](custom-case-headers.md) {% endcontent-ref %} + +{% content-ref url="custom-audit-events.md" %} +[custom-audit-events.md](custom-audit-events.md) +{% endcontent-ref %} diff --git a/documentation/features/case/for-developers/custom-audit-events.md b/documentation/features/case/for-developers/custom-audit-events.md new file mode 100644 index 0000000000..ccf8d5c8b3 --- /dev/null +++ b/documentation/features/case/for-developers/custom-audit-events.md @@ -0,0 +1,112 @@ +# Custom audit events + +This page explains how custom events can be shown in the audit tab on the case detail page. + +## Introduction + +The audit tab on the case detail page shows a history of events that occurred for a case. By default, Valtimo includes 15 built-in event types (such as document created, task completed, process started). When building a plugin or extension that publishes its own audit events, those events will not appear in the audit tab unless they are registered. + +The `DocumentAuditEventProvider` interface makes this possible. Implement it as a Spring bean to register additional event types that Valtimo will include in the audit query. + +## Registering custom audit events + +### Step 1: Publish an audit event + +Create an event class that extends `AuditMetaData` and implements `AuditEvent`, then publish it via Spring's `ApplicationEventPublisher`. + +```kotlin +import com.ritense.valtimo.contract.audit.AuditEvent +import com.ritense.valtimo.contract.audit.AuditMetaData +import java.time.LocalDateTime +import java.util.UUID + +class MyCustomEvent( + id: UUID, + origin: String, + occurredOn: LocalDateTime, + user: String, + val documentId: UUID, + val description: String, +) : AuditMetaData(id, origin, occurredOn, user), AuditEvent +``` + +Publish it from a service: + +```kotlin +import org.springframework.context.ApplicationEventPublisher + +@Service +class MyService(private val eventPublisher: ApplicationEventPublisher) { + + fun doSomething(documentId: UUID) { + // ... business logic ... + eventPublisher.publishEvent( + MyCustomEvent( + id = UUID.randomUUID(), + origin = "MyService", + occurredOn = LocalDateTime.now(), + user = currentUser, + documentId = documentId, + description = "Something happened", + ) + ) + } +} +``` + +### Step 2: Register a `DocumentAuditEventProvider` bean + +```kotlin +import com.ritense.processdocument.service.DocumentAuditEventProvider +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class MyAuditConfiguration { + + @Bean + fun myAuditEventProvider() = DocumentAuditEventProvider { + listOf(MyCustomEvent::class.java) + } +} +``` + +Multiple providers can coexist. Valtimo collects them all and merges the event type lists, so registering a provider never interferes with other providers or the built-in events. + +### Step 3: Register translations via `CASE_AUDIT_TRANSLATION_TOKEN` + +The audit tab uses the simple class name of the event as a translation key under the `events` namespace. The recommended way to supply translations is via the `CASE_AUDIT_TRANSLATION_TOKEN` injection token exported from `@valtimo/case`. Add a provider in your `app.module.ts` (or any Angular module): + +```typescript +import {CASE_AUDIT_TRANSLATION_TOKEN} from '@valtimo/case'; + +// In your NgModule providers array: +{ + provide: CASE_AUDIT_TRANSLATION_TOKEN, + useValue: { + en: {MyCustomEvent: 'Something happened: {{description}}'}, + nl: {MyCustomEvent: 'Er is iets gebeurd: {{description}}'}, + }, + multi: true, +} +``` + +All fields on the event object are available as interpolation parameters in the translation string. Multiple providers can be registered — each one is merged independently under the `events` namespace. + +{% hint style="info" %} +**Scoping guarantee:** The token only accepts `{ [eventClassName]: string }` per language — the `events.` namespace prefix is always added by the framework. It is not possible to accidentally overwrite unrelated translation keys. +{% endhint %} + +#### Alternative: static JSON translation files + +For simple single-language cases you can also add the translation directly to the i18n asset files of the front-end implementation: + +**`assets/i18n/en.json`** + +```json +{ + "events": { + "MyCustomEvent": "Something happened: {{description}}" + } +} +``` diff --git a/documentation/release-notes/13.x.x/13.20.0/README.md b/documentation/release-notes/13.x.x/13.20.0/README.md index 9abbd0d976..7797bd278d 100644 --- a/documentation/release-notes/13.x.x/13.20.0/README.md +++ b/documentation/release-notes/13.x.x/13.20.0/README.md @@ -12,9 +12,9 @@ ## Enhancements -* **New enhancement title** +* **Custom audit events can now be added to the case audit tab** - New enhancement explanation. + Third-party developers can now register additional event types to be shown in the audit tab on the case detail page. Implement the new `DocumentAuditEventProvider` interface as a Spring bean and return the event classes to include. Multiple providers can be registered independently — Valtimo merges them all without affecting the built-in events. See [Custom audit events](../../../features/case/for-developers/custom-audit-events.md) for a step-by-step guide. ## Bugfixes diff --git a/frontend/projects/valtimo/case/src/lib/components/case-detail/tab/audit/audit.component.ts b/frontend/projects/valtimo/case/src/lib/components/case-detail/tab/audit/audit.component.ts index 34783ef3b0..5c90391dba 100644 --- a/frontend/projects/valtimo/case/src/lib/components/case-detail/tab/audit/audit.component.ts +++ b/frontend/projects/valtimo/case/src/lib/components/case-detail/tab/audit/audit.component.ts @@ -14,12 +14,17 @@ * limitations under the License. */ -import {Component, EventEmitter, OnInit, Output} from '@angular/core'; +import {Component, EventEmitter, Inject, OnInit, Optional, Output} from '@angular/core'; import {TimelineItem, TimelineItemImpl} from '@valtimo/components'; import moment from 'moment'; import {ActivatedRoute} from '@angular/router'; import {DocumentService, AuditEvent} from '@valtimo/document'; import {NgxSpinnerService} from 'ngx-spinner'; +import {TranslateService} from '@ngx-translate/core'; +import { + AuditEventTranslations, + CASE_AUDIT_TRANSLATION_TOKEN, +} from '../../../../constants/case-audit-translation-token'; moment.locale(localStorage.getItem('langKey') || ''); moment.defaultFormat = 'DD MMM YYYY HH:mm'; @@ -41,7 +46,11 @@ export class CaseDetailTabAuditComponent implements OnInit { constructor( private route: ActivatedRoute, private documentService: DocumentService, - private spinnerService: NgxSpinnerService + private spinnerService: NgxSpinnerService, + @Optional() + @Inject(CASE_AUDIT_TRANSLATION_TOKEN) + private readonly auditTranslations: AuditEventTranslations[] | null, + private translateService: TranslateService ) { this.spinnerService.show('auditSpinner'); const snapshot = this.route.snapshot.paramMap; @@ -55,7 +64,12 @@ export class CaseDetailTabAuditComponent implements OnInit { } public ngOnInit(): void { - this.loadAuditPage(this.defaultAuditPage); + this.auditTranslations?.forEach(translations => { + Object.entries(translations).forEach(([lang, eventMap]) => { + this.translateService.setTranslation(lang, {events: eventMap}, true); + }); + }); +this.loadAuditPage(this.defaultAuditPage); } public loadAuditPage(pageNumber: number): void { diff --git a/frontend/projects/valtimo/case/src/lib/constants/case-audit-translation-token.ts b/frontend/projects/valtimo/case/src/lib/constants/case-audit-translation-token.ts new file mode 100644 index 0000000000..120c11b5c9 --- /dev/null +++ b/frontend/projects/valtimo/case/src/lib/constants/case-audit-translation-token.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2015-2026 Ritense BV, the Netherlands. + * + * Licensed under EUPL, Version 1.2 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {InjectionToken} from '@angular/core'; + +export type AuditEventTranslations = { + [languageKey: string]: {[eventClassName: string]: string}; +}; + +const CASE_AUDIT_TRANSLATION_TOKEN = new InjectionToken( + 'Provide translations for custom audit events shown in the case detail audit tab.' +); + +export {CASE_AUDIT_TRANSLATION_TOKEN}; diff --git a/frontend/projects/valtimo/case/src/lib/constants/index.ts b/frontend/projects/valtimo/case/src/lib/constants/index.ts index bc346ea0cf..3f3c800b65 100644 --- a/frontend/projects/valtimo/case/src/lib/constants/index.ts +++ b/frontend/projects/valtimo/case/src/lib/constants/index.ts @@ -15,6 +15,7 @@ */ export * from './tab'; +export * from './case-audit-translation-token'; export * from './case-tab-token'; export * from './case-list.constants'; export * from './case-status.constants';