diff --git a/.blueprint/generate-sample/templates/samples/postgresql-mvc-jwt.jdl b/.blueprint/generate-sample/templates/samples/postgresql-mvc-jwt.jdl index d5c164e..f04381b 100644 --- a/.blueprint/generate-sample/templates/samples/postgresql-mvc-jwt.jdl +++ b/.blueprint/generate-sample/templates/samples/postgresql-mvc-jwt.jdl @@ -6,6 +6,7 @@ application { devDatabaseType h2Disk enableHibernateCache false enableTranslation false + jwtSecretKey "ZjY4MTM4YjI5YzMwZjhjYjI2OTNkNTRjMWQ5Y2Q0Y2YwOWNmZTE2NzRmYzU3NTMwM2NjOTE3MTllOTM3MWRkMzcyYTljMjVmNmQ0Y2MxOTUzODc0MDhhMTlkMDIxMzI2YzQzZDM2ZDE3MmQ3NjVkODk3OTVmYzljYTQyZDNmMTQ=" testFrameworks [cypress] } @@ -13,12 +14,14 @@ application { } @EnableAudit +@ChangelogDate(20240000000000) entity Blog { name String required minlength(3) handle String required minlength(2) } @EnableAudit +@ChangelogDate(20240000000100) entity Post { title String required content TextBlob required @@ -26,6 +29,7 @@ entity Post { } @EnableAudit +@ChangelogDate(20240000000300) entity Tag { name String required minlength(2) } diff --git a/generators/angular-audit/generator.js b/generators/angular-audit/generator.js index 53de214..b84cb81 100644 --- a/generators/angular-audit/generator.js +++ b/generators/angular-audit/generator.js @@ -67,7 +67,7 @@ export default class extends BaseApplicationGenerator { source.addItemToAdminMenu?.({ icon: 'list-alt', route: 'admin/entity-audit', - translationKey: 'entityAudit', + translationKey: 'global.menu.admin.entityAudit', name: 'Entity Audit', }); }, diff --git a/generators/angular-audit/resources/package.json b/generators/angular-audit/resources/package.json index 7658ecf..f7f05af 100644 --- a/generators/angular-audit/resources/package.json +++ b/generators/angular-audit/resources/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "ngx-diff": "6.0.1" + "ngx-diff": "8.0.4" } } diff --git a/generators/angular-audit/templates/src/main/webapp/app/admin/entity-audit/entity-audit-modal.component.html.ejs b/generators/angular-audit/templates/src/main/webapp/app/admin/entity-audit/entity-audit-modal.component.html.ejs index 8613d79..cd3a595 100644 --- a/generators/angular-audit/templates/src/main/webapp/app/admin/entity-audit/entity-audit-modal.component.html.ejs +++ b/generators/angular-audit/templates/src/main/webapp/app/admin/entity-audit/entity-audit-modal.component.html.ejs @@ -1,44 +1,27 @@ diff --git a/generators/angular-audit/templates/src/main/webapp/app/admin/entity-audit/entity-audit-modal.component.ts.ejs b/generators/angular-audit/templates/src/main/webapp/app/admin/entity-audit/entity-audit-modal.component.ts.ejs index 5fb8537..c7e0a16 100644 --- a/generators/angular-audit/templates/src/main/webapp/app/admin/entity-audit/entity-audit-modal.component.ts.ejs +++ b/generators/angular-audit/templates/src/main/webapp/app/admin/entity-audit/entity-audit-modal.component.ts.ejs @@ -1,66 +1,52 @@ import { Component } from '@angular/core'; import { HttpResponse } from '@angular/common/http'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { InlineDiffComponent } from 'ngx-diff'; +import { UnifiedDiffComponent } from 'ngx-diff'; import SharedModule from 'app/shared/shared.module'; import { EntityAuditService } from './entity-audit.service'; import { EntityAuditEvent } from './entity-audit-event.model'; @Component({ - standalone: true, - selector: '<%= jhiPrefixDashed %>-entity-audit-modal', - templateUrl: './entity-audit-modal.component.html', - imports: [SharedModule, InlineDiffComponent], - styles: [` - /* NOTE: for now the ::ng-deep shadow-piercing descendant combinator is - * required because Angular defaults to emulated view encapsulation and - * preprocesses all component styles to approximate shadow scoping - * rules. This means these styles wouldn't apply to the HTML generated - * by ng-diff-match-patch. - * - * This shouldn't be required when browsers support native - * encapsulation, at which point ::ng-deep will also be deprecated/removed - * see https://angular.io/guide/component-styles - */ - - :host ::ng-deep ins { - color: black; - background: #bbffbb; - } - - :host ::ng-deep del { - color: black; - background: #ffbbbb; - } - - .code { - background: #dcdada; - padding: 10px; - } - `] + standalone: true, + selector: '<%= jhiPrefixDashed %>-entity-audit-modal', + templateUrl: './entity-audit-modal.component.html', + imports: [SharedModule, UnifiedDiffComponent], + styles: [ + ` + @import 'ngx-diff/styles/default-theme'; + + ins { + color: black; + background-color: var(--ngx-diff-inserted-background-color); + } + + del { + color: black; + background-color: var(--ngx-diff-deleted-background-color); + } + `, + ], }) export default class EntityAuditModalComponent { - action?: string; - left?: string; - right?: string; - - constructor( - private service: EntityAuditService, - public activeModal: NgbActiveModal - ) {} - - openChange(audit: EntityAuditEvent): void { - this.service.getPrevVersion( - audit.entityType, audit.entityId, audit.commitVersion! - ).subscribe((res: HttpResponse) => { - const data: EntityAuditEvent = res.body!; - const previousVersion = JSON.stringify(JSON.parse(data.entityValue ?? '{}'), null, 2); - const currentVersion = JSON.stringify(audit.entityValue, null, 2); - - this.action = audit.action; - this.left = previousVersion; - this.right = currentVersion; - }); - } + action?: string; + left?: string; + right?: string; + + constructor( + private service: EntityAuditService, + public activeModal: NgbActiveModal, + ) {} + + openChange(audit: EntityAuditEvent): void { + this.service.getPrevVersion(audit.entityType, audit.entityId, audit.commitVersion!).subscribe((res: HttpResponse) => { + const data: EntityAuditEvent = res.body!; + const previousVersion = JSON.stringify(JSON.parse(data.entityValue ?? '{}'), null, 2); + const currentVersion = JSON.stringify(audit.entityValue, null, 2); + + this.action = audit.action; + this.left = previousVersion; + this.right = currentVersion; + }); + } } diff --git a/generators/angular-audit/templates/src/main/webapp/app/admin/entity-audit/entity-audit.component.html.ejs b/generators/angular-audit/templates/src/main/webapp/app/admin/entity-audit/entity-audit.component.html.ejs index 2a35aec..46e6e0e 100644 --- a/generators/angular-audit/templates/src/main/webapp/app/admin/entity-audit/entity-audit.component.html.ejs +++ b/generators/angular-audit/templates/src/main/webapp/app/admin/entity-audit/entity-audit.component.html.ejs @@ -1,110 +1,111 @@
- <%= jhiPrefix %>Translate="entityAudit.home.title"<% } %>>Entity Audit +

__jhiTranslateTag__('entityAudit.home.title')

<<%= jhiPrefixDashed %>-alert>-alert>
- <%= jhiPrefix %>Translate="entityAudit.home.filter"<% } %>>Filter +

__jhiTranslateTag__('entityAudit.home.filter')

- - + + @for (entityOption of entities(); track $index) { + + } - - + @for (limitOption of limits; track $index) { + + } -
-
-
-

<%= jhiPrefix %>Translate="entityAudit.result.showInfo" [translateValues]="{ limit: selectedLimit, entity: selectedEntity }"<% } %>> - Last {{ selectedLimit }} - Changes for {{ selectedEntity }} -

+
+ @if (audits().length > 0 || filterEntityId()) { +
+

__jhiTranslateTag__('entityAudit.result.showInfo', { "limit": "selectedLimit()", "entity": "selectedEntity()" })

-
+
- - __jhiTranslateTag__('entityAudit.result.searchFieldLabel') + + placeholder="{{ 'entityAudit.result.entityIdFilter' | translate }}" + [ngModel]="filterEntityId()" + (ngModelChange)="filterEntityId.set($event)" + />
-
+ - +
- - - - - <%= jhiPrefix %>Translate="entityAudit.result.tableHeader.value"<% } %>> - Value - - - + + + + + + + - + - - - - - - - - - + @for (audit of audits(); track audit.id) { + + + + + + + + + + } -
<%= jhiPrefix %>Translate="entityAudit.result.tableHeader.entityId"<% } %>> - Entity Id - <%= jhiPrefix %>Translate="entityAudit.result.tableHeader.action"<% } %>> - Action - <%= jhiPrefix %>Translate="entityAudit.result.tableHeader.version"<% } %>> - Version - <%= jhiPrefix %>Translate="entityAudit.result.tableHeader.modifiedDate"<% } %>> - Modified Date - <%= jhiPrefix %>Translate="entityAudit.result.tableHeader.modifiedBy"<% } %>> - Modified By -
__jhiTranslateTag__('entityAudit.result.tableHeader.entityId')__jhiTranslateTag__('entityAudit.result.tableHeader.action')__jhiTranslateTag__('entityAudit.result.tableHeader.version')__jhiTranslateTag__('entityAudit.result.tableHeader.value')__jhiTranslateTag__('entityAudit.result.tableHeader.modifiedDate')__jhiTranslateTag__('entityAudit.result.tableHeader.modifiedBy')
{{ audit.entityId }}{{ audit.action }}{{ audit.commitVersion }}
{{ audit.entityValue | json }}
{{ audit.modifiedDate | date:'medium' }}{{ audit.modifiedBy }} - -
{{ audit.entityId }}{{ audit.action }}{{ audit.commitVersion }} +
{{ audit.entityValue | json }}
+
{{ audit.modifiedDate | date: 'medium' }}{{ audit.modifiedBy }} + +
+
-

<%= jhiPrefix %>Translate="entityAudit.result.noDataFound"<% } %> class="mt-2"> - No Data found for the filters -

+ } @else { +

__jhiTranslateTag__('entityAudit.result.noDataFound')

+ }
diff --git a/generators/angular-audit/templates/src/main/webapp/app/admin/entity-audit/entity-audit.component.ts.ejs b/generators/angular-audit/templates/src/main/webapp/app/admin/entity-audit/entity-audit.component.ts.ejs index 59b4c96..ef0f807 100644 --- a/generators/angular-audit/templates/src/main/webapp/app/admin/entity-audit/entity-audit.component.ts.ejs +++ b/generators/angular-audit/templates/src/main/webapp/app/admin/entity-audit/entity-audit.component.ts.ejs @@ -1,107 +1,108 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, computed, inject, signal } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { FormsModule } from '@angular/forms'; import { tap } from 'rxjs'; import SharedModule from 'app/shared/shared.module'; +import { AlertService } from 'app/core/util/alert.service'; import { EntityAuditService } from './entity-audit.service'; import { EntityAuditEvent } from './entity-audit-event.model'; import EntityAuditModalComponent from './entity-audit-modal.component'; -import { AlertService } from 'app/core/util/alert.service'; @Component({ - standalone: true, - selector: '<%= jhiPrefixDashed %>-entity-audit', - templateUrl: './entity-audit.component.html', - imports: [SharedModule, FormsModule, EntityAuditModalComponent], - styles: [` + standalone: true, + selector: '<%= jhiPrefixDashed %>-entity-audit', + templateUrl: './entity-audit.component.html', + imports: [SharedModule, FormsModule, EntityAuditModalComponent], + styles: [ + ` .code { background: #dcdada; padding: 10px; } - `], + `, + ], }) export default class EntityAuditComponent implements OnInit { - audits: EntityAuditEvent[] = []; - entities: string[] = []; - selectedEntity?: string; - limits = [25, 50, 100, 200]; - selectedLimit = this.limits[0]; - loading = false; - filterEntityId = ''; - orderProp: keyof EntityAuditEvent = 'entityId'; - ascending = true; + entities = signal([]); + selectedEntity = signal(undefined); + limits = [25, 50, 100, 200]; + selectedLimit = signal(this.limits[0]); + loading = signal(false); + filterEntityId = signal(''); - constructor( - private modalService: NgbModal, - private service: EntityAuditService, - private alertService: AlertService - ) {} + audits = computed(() => { + const filterEntityId = this.filterEntityId(); + const orderProp = this.orderProp(); + const ascending = this.ascending(); + return this._audits() + .filter(audit => !filterEntityId || audit.entityId.toString() === filterEntityId) + .sort((a, b) => { + const aOrderProp = a[orderProp]; + const bOrderProp = b[orderProp]; + if (aOrderProp === bOrderProp) { + return 0; + } + if (aOrderProp === undefined || aOrderProp < bOrderProp) { + return ascending ? -1 : 1; + } + return ascending ? 1 : -1; + }); + }); - ngOnInit(): void { - this.service.getAllAudited().subscribe(entities => { - this.entities = entities; - }); - } + private _audits = signal([]); + private orderProp = signal('entityId'); + private ascending = signal(true); - loadChanges(): void { - if (!this.selectedEntity) { - return; - } - this.loading = true; - this.service.findByEntity(this.selectedEntity, this.selectedLimit) - .pipe(tap(() => (this.loading = false))) - .subscribe(res => { - const data = res.body ?? []; - this.audits = data.map((it: EntityAuditEvent) => { - it.entityValue = JSON.parse(it.entityValue ?? '{}'); - return it; - }); - }); - } + private modalService = inject(NgbModal); + private service = inject(EntityAuditService); + private alertService = inject(AlertService); - trackId(index: number, item: EntityAuditEvent): number { - return item.id; - } + ngOnInit(): void { + this.service.getAllAudited().subscribe(entities => { + this.entities.set(entities); + }); + } - openChange(audit: EntityAuditEvent): void { - if (!audit.commitVersion || audit.commitVersion < 2) { - this.alertService.addAlert({ - type: 'warning', - <%_ if (enableTranslation) { _%> - translationKey: 'entityAudit.result.firstAuditEntry', - <%_ } else { _%> - message: 'There is no previous version available for this entry.\n' + - 'This is the first audit entry captured for this object.', - <%_ } _%> - }); - } else { - const modalRef = this.modalService.open(EntityAuditModalComponent); - modalRef.componentInstance.openChange(audit); - } + loadChanges(): void { + const selectedEntity = this.selectedEntity(); + if (!selectedEntity) { + return; } + this.loading.set(true); + this.service + .findByEntity(selectedEntity, this.selectedLimit()) + .pipe(tap(() => this.loading.set(false))) + .subscribe(res => { + const data = res.body ?? []; + this._audits.set( + data.map((it: EntityAuditEvent) => { + it.entityValue = JSON.parse(it.entityValue ?? '{}'); + return it; + }), + ); + }); + } - orderBy(orderProp: keyof EntityAuditEvent): void { - this.ascending = this.orderProp === orderProp ? !this.ascending : true; - this.orderProp = orderProp; + openChange(audit: EntityAuditEvent): void { + if (!audit.commitVersion || audit.commitVersion < 2) { + this.alertService.addAlert({ + type: 'warning', +<%_ if (enableTranslation) { _%> + translationKey: 'entityAudit.result.firstAuditEntry', +<%_ } else { _%> + message: 'There is no previous version available for this entry.\n' + + 'This is the first audit entry captured for this object.', +<%_ } _%> + }); + } else { + const modalRef = this.modalService.open(EntityAuditModalComponent, { size: 'lg' }); + modalRef.componentInstance.openChange(audit); } + } - getAudits(): EntityAuditEvent[] { - return this.audits - .filter(audit => !this.filterEntityId || audit.entityId.toString() === this.filterEntityId) - .sort((a, b) => { - const aOrderProp = a[this.orderProp]; - const bOrderProp = b[this.orderProp]; - if ( - (aOrderProp === undefined && bOrderProp === undefined) || - (aOrderProp !== undefined && bOrderProp !== undefined && aOrderProp === bOrderProp) - ) { - return 0; - } - if (aOrderProp === undefined || aOrderProp < bOrderProp) { - return this.ascending ? -1 : 1; - } - return this.ascending ? 1 : -1; - }); - } + orderBy(orderProp: keyof EntityAuditEvent): void { + this.ascending.update(ascending => (this.orderProp() === orderProp ? !ascending : true)); + this.orderProp.set(orderProp); + } } diff --git a/generators/languages/generator.js b/generators/languages/generator.js index eaf79ba..7621f7b 100644 --- a/generators/languages/generator.js +++ b/generators/languages/generator.js @@ -13,10 +13,13 @@ export default class extends BaseApplicationGenerator { get [BaseApplicationGenerator.WRITING]() { return this.asWritingTaskGroup({ - async writingTemplateTask({ application: { languages = [], clientSrcDir } }) { + async writingTemplateTask({ application: { languages = [], nativeLanguage, clientSrcDir } }) { if (!clientSrcDir) { throw new Error('clientSrcDir is missing'); } + if (!languages.includes(nativeLanguage)) { + languages.unshift(nativeLanguage); + } const templates = languages.map(language => { const sourceLanguage = existsSync(`${this.templatePath()}/${TEMPLATES_WEBAPP_SOURCES_DIR}i18n/${language}/entity-audit.json`) ? language diff --git a/generators/spring-boot-javers/generator.js b/generators/spring-boot-javers/generator.js index df78410..c536901 100644 --- a/generators/spring-boot-javers/generator.js +++ b/generators/spring-boot-javers/generator.js @@ -21,7 +21,9 @@ export default class extends BaseApplicationGenerator { get [BaseApplicationGenerator.DEFAULT]() { return this.asDefaultTaskGroup({ async defaultTask({ application, entities }) { - application.auditedEntities = entities.map(e => e.persistClass); + application.auditedEntities = entities + .filter(e => e.enableAudit) + .map(e => `${e.entityPackage ? `${e.entityPackage}.` : ''}domain.${e.persistClass}`); }, }); } diff --git a/generators/spring-boot-javers/templates/src/main/java/_package_/web/rest/JaversEntityAuditResource.java.ejs b/generators/spring-boot-javers/templates/src/main/java/_package_/web/rest/JaversEntityAuditResource.java.ejs index 92495c7..c462370 100644 --- a/generators/spring-boot-javers/templates/src/main/java/_package_/web/rest/JaversEntityAuditResource.java.ejs +++ b/generators/spring-boot-javers/templates/src/main/java/_package_/web/rest/JaversEntityAuditResource.java.ejs @@ -5,15 +5,19 @@ import <%=packageName%>.security.AuthoritiesConstants; import tech.jhipster.web.util.PaginationUtil; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; import org.javers.core.Javers; import org.javers.core.metamodel.object.CdoSnapshot; import org.javers.repository.jql.QueryBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.core.convert.ConversionService; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -39,10 +43,16 @@ public class JaversEntityAuditResource { private final Logger log = LoggerFactory.getLogger(JaversEntityAuditResource.class); + @PersistenceContext + private EntityManager manager; + private final Javers javers; - public JaversEntityAuditResource(Javers javers) { + private final ConversionService conversionService; + + public JaversEntityAuditResource(Javers javers, ConversionService conversionService) { this.javers = javers; + this.conversionService = conversionService; } /** @@ -75,7 +85,7 @@ public class JaversEntityAuditResource { throws ClassNotFoundException { log.debug("REST request to get a page of EntityAuditEvents"); - Class entityTypeToFetch = Class.forName("<%=packageName%>.domain." + entityType); + var entityTypeToFetch = Class.forName("<%=packageName%>." + entityType); QueryBuilder jqlQuery = QueryBuilder.byClass(entityTypeToFetch) .limit(limit); @@ -111,13 +121,12 @@ public class JaversEntityAuditResource { @RequestParam(value = "commitVersion") Long commitVersion) throws ClassNotFoundException { - Class entityTypeToFetch = Class.forName("<%=packageName%>.domain." + qualifiedName); - - QueryBuilder jqlQuery = QueryBuilder.byInstanceId(entityId, entityTypeToFetch) - .limit(1) - .withVersion(commitVersion - 1); + var entityTypeToFetch = Class.forName("<%- packageName %>." + qualifiedName); + var entityInformation = JpaEntityInformationSupport.getEntityInformation(entityTypeToFetch, manager); + var id = conversionService.convert(entityId, entityInformation.getIdType()); - EntityAuditEvent prev = EntityAuditEvent.fromJaversSnapshot(javers.findSnapshots(jqlQuery.build()).get(0)); + var jqlQuery = QueryBuilder.byInstanceId(id, entityTypeToFetch).limit(1).withVersion(commitVersion - 1); + var prev = EntityAuditEvent.fromJaversSnapshot(javers.findSnapshots(jqlQuery.build()).get(0)); return new ResponseEntity<>(prev, HttpStatus.OK); diff --git a/vitest.config.ts b/vitest.config.ts index d46556a..d1fa83c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,5 +5,6 @@ export default defineConfig({ pool: 'forks', hookTimeout: 20000, exclude: [...defaultExclude.filter(val => val !== '**/cypress/**'), '**/templates/**', '**/resources/**'], + setupFiles: ['./vitest.test-setup.ts'], }, }); diff --git a/vitest.test-setup.ts b/vitest.test-setup.ts new file mode 100644 index 0000000..b94ccb0 --- /dev/null +++ b/vitest.test-setup.ts @@ -0,0 +1,6 @@ +import { vi } from 'vitest'; +import { defineDefaults } from 'generator-jhipster/testing'; + +defineDefaults({ + mockFactory: () => vi.fn(), +});