From 6ae4f0fbce5ccfa00b790901436c9ed22453b7ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henry=20T=C3=A4schner?= <129834483+HenryT-CG@users.noreply.github.com> Date: Sun, 22 Dec 2024 09:00:03 +0100 Subject: [PATCH] Fix: align code with parameter mgmt (#315) * fix: align search and detail including tests with parameter mgmt * fix: align search and detail including tests with parameter mgmt * fix: extend tests * fix: tests --- src/app/_am-mixins.scss | 1 + .../announcement-detail.component.html | 19 +- .../announcement-detail.component.spec.ts | 310 ++++++----- .../announcement-detail.component.ts | 81 +-- .../announcement-criteria.component.html | 18 +- .../announcement-criteria.component.spec.ts | 42 +- .../announcement-criteria.component.ts | 37 +- .../announcement-search.component.html | 484 +++++++++--------- .../announcement-search.component.scss | 3 +- .../announcement-search.component.spec.ts | 367 +++++++------ .../announcement-search.component.ts | 223 ++++---- src/app/announcement/announcement.module.ts | 7 +- src/app/shared/shared.module.ts | 5 +- src/assets/i18n/de.json | 114 ++++- src/assets/i18n/en.json | 116 ++++- tsconfig.app.json | 2 +- webpack.config.js | 1 + 17 files changed, 1031 insertions(+), 799 deletions(-) diff --git a/src/app/_am-mixins.scss b/src/app/_am-mixins.scss index 11fbe60..bb4ca66 100644 --- a/src/app/_am-mixins.scss +++ b/src/app/_am-mixins.scss @@ -48,6 +48,7 @@ } } } + @mixin displaying-text-ellipsis($lines: 1) { text-overflow: ellipsis; overflow: hidden; diff --git a/src/app/announcement/announcement-detail/announcement-detail.component.html b/src/app/announcement/announcement-detail/announcement-detail.component.html index 227ec07..bf94468 100644 --- a/src/app/announcement/announcement-detail/announcement-detail.component.html +++ b/src/app/announcement/announcement-detail/announcement-detail.component.html @@ -1,6 +1,6 @@ +
- - {{ 'DIALOG.DETAIL.ANNOUNCEMENT.PROPERTIES' | translate }} + + {{ 'DIALOG.DETAIL.TAB.PROPS' | translate }}
@@ -128,7 +125,7 @@ tooltipEvent="hover" > - +
@@ -150,7 +147,7 @@ tooltipEvent="hover" > - +
@@ -205,7 +202,7 @@ tooltipPosition="top" tooltipEvent="hover" > - + diff --git a/src/app/announcement/announcement-detail/announcement-detail.component.spec.ts b/src/app/announcement/announcement-detail/announcement-detail.component.spec.ts index 990d933..97e9c3e 100644 --- a/src/app/announcement/announcement-detail/announcement-detail.component.spec.ts +++ b/src/app/announcement/announcement-detail/announcement-detail.component.spec.ts @@ -6,12 +6,8 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core' import { of, throwError } from 'rxjs' import { FormControl, FormGroup } from '@angular/forms' -import { - AppStateService, - createTranslateLoader, - PortalMessageService, - UserService -} from '@onecx/portal-integration-angular' +import { AppStateService, UserService } from '@onecx/angular-integration-interface' +import { createTranslateLoader, PortalMessageService } from '@onecx/portal-integration-angular' import { Announcement, @@ -54,16 +50,11 @@ describe('AnnouncementDetailComponent', () => { searchProductsByCriteria: jasmine.createSpy('searchProductsByCriteria').and.returnValue(of([])) } const formGroup = new FormGroup({ - id: new FormControl('id'), title: new FormControl('title'), workspaceName: new FormControl('workspace name'), productName: new FormControl('prod name') }) - const mockUserService = { - lang$: { - getValue: jasmine.createSpy('getValue') - } - } + const mockUserService = { lang$: { getValue: jasmine.createSpy('getValue') } } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -102,7 +93,7 @@ describe('AnnouncementDetailComponent', () => { fixture = TestBed.createComponent(AnnouncementDetailComponent) component = fixture.componentInstance fixture.detectChanges() - component.displayDetailDialog = true + component.displayDialog = true }) afterEach(() => { @@ -115,154 +106,230 @@ describe('AnnouncementDetailComponent', () => { it('should create but not initialize if dialog is not open', () => { expect(component).toBeTruthy() - component.displayDetailDialog = false + component.displayDialog = false component.ngOnChanges() }) - describe('ngOnChange, i.e. opening detail dialog', () => { - it('should prepare editing/viewing an announcement - successful', () => { - apiServiceSpy.getAnnouncementById.and.returnValue(of(announcement)) - component.changeMode = 'EDIT' - component.announcement = announcement + describe('ngOnChange - init form', () => { + describe('VIEW', () => { + it('should reject initializing if dialog is not open', () => { + apiServiceSpy.getAnnouncementById.and.returnValue(of(announcement)) + component.announcement = announcement + component.changeMode = 'VIEW' + component.displayDialog = false + + component.ngOnChanges() - component.ngOnChanges() + expect(apiServiceSpy.getAnnouncementById).not.toHaveBeenCalled() + }) - expect(apiServiceSpy.getAnnouncementById).toHaveBeenCalled() - expect(component.formGroup.enabled).toBeTrue() - expect(component.formGroup.controls['id'].value).toEqual(announcement.id) - expect(component.formGroup.controls['content'].value).toEqual(announcement.content) - expect(component.formGroup.controls['startDate'].value).not.toBeNull() + it('should reject initializing if data is missed', () => { + apiServiceSpy.getAnnouncementById.and.returnValue(of(announcement)) + component.announcement = undefined + component.changeMode = 'VIEW' - component.changeMode = 'VIEW' - component.announcement = announcement + component.ngOnChanges() - component.ngOnChanges() + expect(apiServiceSpy.getAnnouncementById).not.toHaveBeenCalled() + }) - expect(apiServiceSpy.getAnnouncementById).toHaveBeenCalled() - expect(component.formGroup.disabled).toBeTrue() - expect(component.formGroup.controls['id'].value).toEqual(announcement.id) - expect(component.formGroup.controls['title'].value).toEqual(announcement.title) - }) + it('should prepare viewing an announcement - successful', () => { + apiServiceSpy.getAnnouncementById.and.returnValue(of(announcement)) + component.announcement = announcement + component.changeMode = 'VIEW' - it('should prepare editing/viewing an announcement - failed: id missed', () => { - component.changeMode = 'EDIT' - component.announcement = { ...announcement, id: undefined } + component.ngOnChanges() - component.ngOnChanges() + expect(apiServiceSpy.getAnnouncementById).toHaveBeenCalled() + expect(component.formGroup.disabled).toBeTrue() + expect(component.formGroup.controls['title'].value).toBe(announcement.title) + expect(component.loading).toBeFalse() + //expect(component.productOptions).toEqual(allProductsSI) + }) + + it('should prepare viewing an announcement - failed: missing id', () => { + apiServiceSpy.getAnnouncementById.and.returnValue(of(announcement)) + component.announcement = { ...announcement, id: undefined } + component.changeMode = 'VIEW' + + component.ngOnChanges() - expect(apiServiceSpy.getAnnouncementById).not.toHaveBeenCalled() - expect(component.formGroup.disabled).toBeTrue() - expect(component.formGroup.controls['id'].value).toBeNull() + expect(apiServiceSpy.getAnnouncementById).not.toHaveBeenCalled() + }) + + it('should prepare viewing an announcement - failed: missing permissions', () => { + const errorResponse = { status: 403, statusText: 'No permissions' } + apiServiceSpy.getAnnouncementById.and.returnValue(throwError(() => errorResponse)) + component.announcement = announcement + component.changeMode = 'VIEW' + spyOn(component.formGroup, 'reset') + spyOn(console, 'error') + + component.ngOnChanges() + + expect(apiServiceSpy.getAnnouncementById).toHaveBeenCalled() + expect(component.formGroup.reset).toHaveBeenCalled() + expect(component.formGroup.disabled).toBeTrue() + expect(component.exceptionKey).toBe('EXCEPTIONS.HTTP_STATUS_' + errorResponse.status + '.ANNOUNCEMENT') + expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: component.exceptionKey }) + expect(console.error).toHaveBeenCalledWith('getAnnouncementById', errorResponse) + }) }) - it('should prepare copying an announcement - start with data from other announcement', () => { - component.changeMode = 'CREATE' - component.announcement = announcement // stopped, id is filled + describe('EDIT', () => { + it('should prepare editing an announcement - successful', () => { + apiServiceSpy.getAnnouncementById.and.returnValue(of(announcement)) + component.changeMode = 'EDIT' + component.announcement = announcement - component.ngOnChanges() + component.ngOnChanges() - expect(component.formGroup.disabled).toBeTrue() - expect(component.formGroup.controls['id'].value).toBeNull() // after reset + expect(apiServiceSpy.getAnnouncementById).toHaveBeenCalled() + expect(component.formGroup.enabled).toBeTrue() + expect(component.formGroup.controls['title'].value).toEqual(announcement.title) + expect(component.formGroup.controls['content'].value).toEqual(announcement.content) + expect(component.formGroup.controls['startDate'].value).not.toBeNull() + }) - component.announcement = { ...announcement, id: undefined } // correct + it('should prepare editing an announcement - failed: id missed', () => { + component.changeMode = 'EDIT' + component.announcement = { ...announcement, id: undefined } - component.ngOnChanges() + component.ngOnChanges() - expect(component.formGroup.enabled).toBeTrue() - expect(component.formGroup.controls['id'].value).toBeUndefined() - expect(component.formGroup.controls['title'].value).toEqual(announcement.title) - }) + expect(apiServiceSpy.getAnnouncementById).not.toHaveBeenCalled() + }) - it('should prepare copying an announcement - without date values', () => { - component.changeMode = 'CREATE' - component.announcement = { ...announcement, id: undefined, startDate: undefined, endDate: undefined } + it('should display error if getting the announcement fails', () => { + const errorResponse = { status: 404, statusText: 'Not Found' } + apiServiceSpy.getAnnouncementById.and.returnValue(throwError(() => errorResponse)) + component.changeMode = 'EDIT' + component.announcement = announcement + spyOn(console, 'error') - component.ngOnChanges() + component.ngOnChanges() - expect(component.formGroup.enabled).toBeTrue() - expect(component.formGroup.controls['title'].value).toEqual(announcement.title) - expect(component.formGroup.controls['startDate'].value).toBeNull() + expect(component.exceptionKey).toEqual('EXCEPTIONS.HTTP_STATUS_' + errorResponse.status + '.ANNOUNCEMENT') + expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: component.exceptionKey }) + expect(console.error).toHaveBeenCalledWith('getAnnouncementById', errorResponse) + }) }) - it('should prepare creating an announcement - start with empty form', () => { - component.changeMode = 'CREATE' - spyOn(component.formGroup, 'reset') + describe('CREATE', () => { + it('should prepare copying an announcement - start with data from other announcement', () => { + component.changeMode = 'CREATE' + component.announcement = announcement // will be rejected due to filled + + component.ngOnChanges() + + expect(apiServiceSpy.getAnnouncementById).not.toHaveBeenCalled() - component.ngOnChanges() + component.announcement = undefined // correct - expect(component.formGroup.reset).toHaveBeenCalled() - // check default values - expect(component.formGroup.controls['priority'].value).toEqual(AnnouncementPriorityType.Normal) - expect(component.formGroup.controls['status'].value).toEqual(AnnouncementStatus.Inactive) - expect(component.formGroup.controls['type'].value).toEqual(AnnouncementType.Info) + component.ngOnChanges() + + expect(component.formGroup.enabled).toBeTrue() + expect(component.formGroup.controls['title'].value).toEqual(null) + }) + + it('should prepare creating an announcement - start with empty form', () => { + component.changeMode = 'CREATE' + spyOn(component.formGroup, 'reset') + + component.ngOnChanges() + + expect(component.formGroup.reset).toHaveBeenCalled() + // check default values + expect(component.formGroup.controls['priority'].value).toEqual(AnnouncementPriorityType.Normal) + expect(component.formGroup.controls['status'].value).toEqual(AnnouncementStatus.Inactive) + expect(component.formGroup.controls['type'].value).toEqual(AnnouncementType.Info) + }) }) - it('should display error if getting the announcement fails', () => { - const errorResponse = { status: 404, statusText: 'Not Found' } - apiServiceSpy.getAnnouncementById.and.returnValue(throwError(() => errorResponse)) - component.changeMode = 'EDIT' - component.announcement = announcement - spyOn(console, 'error') + describe('COPY', () => { + it('should prepare copying an announcement - start with data from other announcement', () => { + component.changeMode = 'COPY' + component.announcement = announcement - component.ngOnChanges() + component.ngOnChanges() - expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.SEARCH.SEARCH_FAILED' }) - expect(console.error).toHaveBeenCalledWith('getAnnouncementById', errorResponse) + expect(apiServiceSpy.getAnnouncementById).not.toHaveBeenCalled() + }) + + it('should prepare copying an announcement - without date values', () => { + component.changeMode = 'COPY' + component.announcement = { ...announcement, id: undefined, startDate: undefined, endDate: undefined } + + component.ngOnChanges() + + expect(component.formGroup.enabled).toBeTrue() + expect(component.formGroup.controls['title'].value).toEqual(announcement.title) + expect(component.formGroup.controls['startDate'].value).toBeNull() + }) }) }) - describe('onSave - creating and updating an announcement', () => { - it('should create an announcement', () => { - apiServiceSpy.createAnnouncement.and.returnValue(of({})) - component.changeMode = 'CREATE' - spyOn(component.hideDialogAndChanged, 'emit') - component.formGroup = formGroup + describe('saving', () => { + describe('CREATE', () => { + it('should create an announcement', () => { + apiServiceSpy.createAnnouncement.and.returnValue(of({})) + component.changeMode = 'CREATE' + spyOn(component.hideDialogAndChanged, 'emit') + component.formGroup = formGroup - component.onSave() + component.onSave() - expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.MESSAGE.OK' }) - expect(component.hideDialogAndChanged.emit).toHaveBeenCalledWith(true) - }) + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.MESSAGE.OK' }) + expect(component.hideDialogAndChanged.emit).toHaveBeenCalledWith(true) + }) - it('should display error if creation fails', () => { - const errorResponse = { status: 400, statusText: 'Could not create ...' } - apiServiceSpy.createAnnouncement.and.returnValue(throwError(() => errorResponse)) - spyOn(console, 'error') - component.changeMode = 'CREATE' - component.formGroup = formGroup + it('should display error if creation fails', () => { + const errorResponse = { status: 400, statusText: 'Could not create ...' } + apiServiceSpy.createAnnouncement.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') + component.changeMode = 'CREATE' + component.formGroup = formGroup - component.onSave() + component.onSave() - expect(component.formGroup.valid).toBeTrue() - expect(console.error).toHaveBeenCalledWith('createAnnouncement', errorResponse) - expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.MESSAGE.NOK' }) + expect(component.formGroup.valid).toBeTrue() + expect(console.error).toHaveBeenCalledWith('createAnnouncement', errorResponse) + expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.CREATE.MESSAGE.NOK' }) + }) }) - it('should update an announcement', () => { - apiServiceSpy.updateAnnouncementById.and.returnValue(of({})) - component.changeMode = 'EDIT' - spyOn(component.hideDialogAndChanged, 'emit') - component.formGroup = formGroup + describe('EDIT', () => { + it('should update an announcement', () => { + apiServiceSpy.updateAnnouncementById.and.returnValue(of({})) + component.changeMode = 'EDIT' + component.announcement = announcement + component.formGroup = formGroup - component.onSave() + spyOn(component.hideDialogAndChanged, 'emit') - expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.MESSAGE.OK' }) - expect(component.hideDialogAndChanged.emit).toHaveBeenCalledWith(true) - }) + component.onSave() - it('should display error if update fails', () => { - const errorResponse = { status: 400, statusText: 'Could not update ...' } - apiServiceSpy.updateAnnouncementById.and.returnValue(throwError(() => errorResponse)) - spyOn(console, 'error') - component.changeMode = 'EDIT' - component.formGroup = formGroup + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.MESSAGE.OK' }) + expect(component.hideDialogAndChanged.emit).toHaveBeenCalledWith(true) + }) - component.onSave() + it('should display error if update fails', () => { + const errorResponse = { status: 400, statusText: 'Could not update ...' } + apiServiceSpy.updateAnnouncementById.and.returnValue(throwError(() => errorResponse)) + spyOn(console, 'error') + component.changeMode = 'EDIT' + component.announcement = announcement + component.formGroup = formGroup - expect(console.error).toHaveBeenCalledWith('updateAnnouncementById', errorResponse) - expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.MESSAGE.NOK' }) + component.onSave() + + expect(console.error).toHaveBeenCalledWith('updateAnnouncementById', errorResponse) + expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.EDIT.MESSAGE.NOK' }) + }) }) + }) + describe('form invalid', () => { it('should display warning when trying to save an invalid announcement', () => { component.formGroup = formGroup component.formGroup.setErrors({ title: true }) @@ -324,12 +391,15 @@ describe('AnnouncementDetailComponent', () => { /* * UI ACTIONS */ - it('should close the dialog', () => { - spyOn(component.hideDialogAndChanged, 'emit') - component.onDialogHide() + describe('Extra UI actions', () => { + describe('Closing dialog', () => { + it('should close the dialog if user triggers hiding', () => { + spyOn(component.hideDialogAndChanged, 'emit') + component.onDialogHide() - expect(component.displayDetailDialog).toBeFalse() - expect(component.hideDialogAndChanged.emit).toHaveBeenCalledWith(false) + expect(component.hideDialogAndChanged.emit).toHaveBeenCalledWith(false) + }) + }) }) /** diff --git a/src/app/announcement/announcement-detail/announcement-detail.component.ts b/src/app/announcement/announcement-detail/announcement-detail.component.ts index 19d7d2f..32aef5c 100644 --- a/src/app/announcement/announcement-detail/announcement-detail.component.ts +++ b/src/app/announcement/announcement-detail/announcement-detail.component.ts @@ -34,7 +34,7 @@ export function dateRangeValidator(fg: FormGroup): ValidatorFn { styleUrls: ['./announcement-detail.component.scss'] }) export class AnnouncementDetailComponent implements OnChanges { - @Input() public displayDetailDialog = false + @Input() public displayDialog = false @Input() public changeMode: ChangeMode = 'CREATE' @Input() public announcement: Announcement | undefined @Input() public allWorkspaces: SelectItem[] = [] @@ -61,9 +61,7 @@ export class AnnouncementDetailComponent implements OnChanges { ) { this.dateFormat = this.user.lang$.getValue() === 'de' ? 'dd.mm.yy' : 'mm/dd/yy' this.timeFormat = this.user.lang$.getValue() === 'de' ? '24' : '12' - this.prepareDropDownOptions() this.formGroup = fb.nonNullable.group({ - id: new FormControl(null), modificationCount: new FormControl(null), title: new FormControl(null, [Validators.required, Validators.minLength(2), Validators.maxLength(255)]), content: new FormControl(null, [Validators.maxLength(1000)]), @@ -78,60 +76,71 @@ export class AnnouncementDetailComponent implements OnChanges { }) this.formGroup.controls['startDate'].addValidators([Validators.required, dateRangeValidator(this.formGroup)]) this.formGroup.controls['endDate'].addValidators([dateRangeValidator(this.formGroup)]) + // prepare dropdown lists + this.prepareDropDownOptions() } public ngOnChanges() { - if (!this.displayDetailDialog) return - this.displayDateRangeError = false - this.formGroup.disable() - if (this.changeMode === 'EDIT' || this.changeMode === 'VIEW') { - if (!this.announcement?.id) return // id is mandatory - // refresh data and fill form - this.getAnnouncement(this.announcement.id) - if (this.changeMode === 'EDIT') this.formGroup.enable() - } - if (this.changeMode === 'CREATE') { - if (this.announcement?.id) return // error - this.fillForm(this.announcement) // on COPY? - this.formGroup.enable() - } + if (!this.displayDialog) return + // matching mode and given data? + if ('CREATE' === this.changeMode && this.announcement) return + if (['EDIT', 'VIEW'].includes(this.changeMode)) + if (!this.announcement) return + else this.getData(this.announcement?.id) + else this.prepareForm(this.announcement) } - public onDialogHide() { - this.displayDetailDialog = false - this.hideDialogAndChanged.emit(false) - this.formGroup.disable() + private prepareForm(data?: Announcement): void { + if (data) { + this.formGroup.patchValue(data) + this.formGroup.controls['startDate'].patchValue(data?.startDate ? new Date(data.startDate) : null) + this.formGroup.controls['endDate'].patchValue(data?.endDate ? new Date(data.endDate) : null) + } + switch (this.changeMode) { + case 'COPY': + this.formGroup.enable() + break + case 'CREATE': + this.formGroup.reset() + this.formGroup.enable() + break + case 'EDIT': + this.formGroup.enable() + break + case 'VIEW': + this.formGroup.disable() + break + } } /** * READING data */ - private getAnnouncement(id: string): void { + private getData(id?: string): void { + if (!id) return this.loading = true this.exceptionKey = undefined this.announcementApi .getAnnouncementById({ id: id }) .pipe(finalize(() => (this.loading = false))) .subscribe({ - next: (data) => this.fillForm(data), + next: (data) => this.prepareForm(data), error: (err) => { this.formGroup.reset() this.formGroup.disable() this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_' + err.status + '.ANNOUNCEMENT' - this.msgService.error({ summaryKey: 'ACTIONS.SEARCH.SEARCH_FAILED' }) + this.msgService.error({ summaryKey: this.exceptionKey }) console.error('getAnnouncementById', err) } }) } - private fillForm(item?: Announcement): void { - if (item) - this.formGroup.patchValue({ - ...item, - startDate: item?.startDate ? new Date(item.startDate) : null, - endDate: item?.endDate ? new Date(item.endDate) : null - }) - else this.formGroup.reset() + /**************************************************************************** + * UI Events + */ + public onDialogHide(changed?: boolean) { + this.hideDialogAndChanged.emit(changed ?? false) + this.formGroup.reset() } /** @@ -146,16 +155,16 @@ export class AnnouncementDetailComponent implements OnChanges { this.msgService.warning({ summaryKey: 'VALIDATION.ERRORS.INVALID_FORM' }) return } - if (this.changeMode === 'EDIT') { + if (this.changeMode === 'EDIT' && this.announcement?.id) { this.announcementApi .updateAnnouncementById({ - id: this.formGroup.controls['id'].value, + id: this.announcement?.id, updateAnnouncementRequest: this.submitFormValues() as UpdateAnnouncementRequest }) .subscribe({ next: () => { this.msgService.success({ summaryKey: 'ACTIONS.EDIT.MESSAGE.OK' }) - this.hideDialogAndChanged.emit(true) + this.onDialogHide(true) }, error: (err) => { this.msgService.error({ summaryKey: 'ACTIONS.EDIT.MESSAGE.NOK' }) @@ -171,7 +180,7 @@ export class AnnouncementDetailComponent implements OnChanges { .subscribe({ next: () => { this.msgService.success({ summaryKey: 'ACTIONS.CREATE.MESSAGE.OK' }) - this.hideDialogAndChanged.emit(true) + this.onDialogHide(true) }, error: (err) => { this.msgService.error({ summaryKey: 'ACTIONS.CREATE.MESSAGE.NOK' }) diff --git a/src/app/announcement/announcement-search/announcement-criteria/announcement-criteria.component.html b/src/app/announcement/announcement-search/announcement-criteria/announcement-criteria.component.html index 0c86409..9656a96 100644 --- a/src/app/announcement/announcement-search/announcement-criteria/announcement-criteria.component.html +++ b/src/app/announcement/announcement-search/announcement-criteria/announcement-criteria.component.html @@ -1,12 +1,12 @@ -
+
- + - + - + ({ priority: new FormControl([AnnouncementPriorityType.Low]), startDateRange: new FormControl([new Date('2023-01-02'), new Date('2023-01-03')]) }) - const emptyCriteria = new FormGroup({ title: new FormControl(null), workspaceName: new FormControl(null), @@ -68,49 +68,49 @@ describe('AnnouncementCriteriaComponent', () => { expect(component).toBeTruthy() }) - describe('onSubmitCriteria & onResetCriteria', () => { + describe('onSearch & onResetCriteria', () => { it('should search announcements without criteria', () => { - component.announcementCriteria = emptyCriteria - spyOn(component.criteriaEmitter, 'emit') + component.criteriaForm = emptyCriteria + spyOn(component.searchEmitter, 'emit') - component.onSubmitCriteria() + component.onSearch() - expect(component.criteriaEmitter.emit).toHaveBeenCalled() + expect(component.searchEmitter.emit).toHaveBeenCalled() }) it('should search announcements with criteria', () => { - component.announcementCriteria = filledCriteria - spyOn(component.criteriaEmitter, 'emit') + component.criteriaForm = filledCriteria + spyOn(component.searchEmitter, 'emit') - component.onSubmitCriteria() + component.onSearch() - expect(component.criteriaEmitter.emit).toHaveBeenCalled() + expect(component.searchEmitter.emit).toHaveBeenCalled() }) it('should prevent user from searching for invalid dates', () => { - component.announcementCriteria = filledCriteria - component.announcementCriteria.patchValue({ startDateRange: [new Date('2023-01-02')] }) - spyOn(component.criteriaEmitter, 'emit') + component.criteriaForm = filledCriteria + component.criteriaForm.patchValue({ startDateRange: [new Date('2023-01-02')] }) + spyOn(component.searchEmitter, 'emit') - component.onSubmitCriteria() + component.onSearch() - expect(component.criteriaEmitter.emit).toHaveBeenCalled() + expect(component.searchEmitter.emit).toHaveBeenCalled() }) it('should reset search criteria', () => { - component.announcementCriteria = filledCriteria - spyOn(component.criteriaEmitter, 'emit') + component.criteriaForm = filledCriteria + spyOn(component.searchEmitter, 'emit') - component.onSubmitCriteria() + component.onSearch() - expect(component.criteriaEmitter.emit).toHaveBeenCalled() + expect(component.searchEmitter.emit).toHaveBeenCalled() spyOn(component.resetSearchEmitter, 'emit') - spyOn(component.announcementCriteria, 'reset') + spyOn(component.criteriaForm, 'reset') component.onResetCriteria() - expect(component.announcementCriteria.reset).toHaveBeenCalled() + expect(component.criteriaForm.reset).toHaveBeenCalled() expect(component.resetSearchEmitter.emit).toHaveBeenCalled() }) }) diff --git a/src/app/announcement/announcement-search/announcement-criteria/announcement-criteria.component.ts b/src/app/announcement/announcement-search/announcement-criteria/announcement-criteria.component.ts index ae90708..e4ece5a 100644 --- a/src/app/announcement/announcement-search/announcement-criteria/announcement-criteria.component.ts +++ b/src/app/announcement/announcement-search/announcement-criteria/announcement-criteria.component.ts @@ -31,12 +31,12 @@ export interface AnnouncementCriteriaForm { export class AnnouncementCriteriaComponent implements OnInit { @Input() public actions: Action[] = [] @Input() public workspaces: SelectItem[] = [] - @Input() public products: SelectItem[] = [] - @Output() public criteriaEmitter = new EventEmitter() + @Input() public usedProducts: SelectItem[] = [] + @Output() public searchEmitter = new EventEmitter() @Output() public resetSearchEmitter = new EventEmitter() public displayCreateDialog = false - public announcementCriteria!: FormGroup + public criteriaForm!: FormGroup public dateFormatForRange: string public filteredTitles = [] public type$: Observable = of([]) @@ -51,7 +51,7 @@ export class AnnouncementCriteriaComponent implements OnInit { } ngOnInit(): void { - this.announcementCriteria = new FormGroup({ + this.criteriaForm = new FormGroup({ title: new FormControl(null), workspaceName: new FormControl(null), productName: new FormControl(null), @@ -123,35 +123,28 @@ export class AnnouncementCriteriaComponent implements OnInit { ) } - public onSubmitCriteria(): void { + public onSearch(): void { const criteriaRequest: SearchAnnouncementsRequestParams = { announcementSearchCriteria: { - title: this.announcementCriteria.value.title === null ? undefined : this.announcementCriteria.value.title, + title: this.criteriaForm.value.title === null ? undefined : this.criteriaForm.value.title, workspaceName: - this.announcementCriteria.value.workspaceName === null - ? undefined - : this.announcementCriteria.value.workspaceName, - productName: - this.announcementCriteria.value.productName === null - ? undefined - : this.announcementCriteria.value.productName, - priority: - this.announcementCriteria.value.priority === null ? undefined : this.announcementCriteria.value.priority?.[0], - status: - this.announcementCriteria.value.status === null ? undefined : this.announcementCriteria.value.status?.[0], - type: this.announcementCriteria.value.type === null ? undefined : this.announcementCriteria.value.type?.[0] + this.criteriaForm.value.workspaceName === null ? undefined : this.criteriaForm.value.workspaceName, + productName: this.criteriaForm.value.productName === null ? undefined : this.criteriaForm.value.productName, + priority: this.criteriaForm.value.priority === null ? undefined : this.criteriaForm.value.priority?.[0], + status: this.criteriaForm.value.status === null ? undefined : this.criteriaForm.value.status?.[0], + type: this.criteriaForm.value.type === null ? undefined : this.criteriaForm.value.type?.[0] } } - if (this.announcementCriteria.value.startDateRange) { - const dates = this.mapDateRangeToDateStrings(this.announcementCriteria.value.startDateRange) + if (this.criteriaForm.value.startDateRange) { + const dates = this.mapDateRangeToDateStrings(this.criteriaForm.value.startDateRange) criteriaRequest.announcementSearchCriteria.startDateFrom = dates[0] criteriaRequest.announcementSearchCriteria.startDateTo = dates[1] } - this.criteriaEmitter.emit(criteriaRequest) + this.searchEmitter.emit(criteriaRequest) } public onResetCriteria(): void { - this.announcementCriteria.reset() + this.criteriaForm.reset() this.resetSearchEmitter.emit(true) } diff --git a/src/app/announcement/announcement-search/announcement-search.component.html b/src/app/announcement/announcement-search/announcement-search.component.html index 38bf781..0fa1e1d 100644 --- a/src/app/announcement/announcement-search/announcement-search.component.html +++ b/src/app/announcement/announcement-search/announcement-search.component.html @@ -1,17 +1,17 @@ - + - - - - - - - - {{ 'ACTIONS.SEARCH.NO_DATA' | translate }} - - - - - - {{ 'ACTIONS.SEARCH.ACTIONS' | translate }} - - - {{ col.translationPrefix + '.' + col.header | translate }} - - - - - + + + + + + - - - - - - - - + + + {{ 'ACTIONS.SEARCH.NO_DATA' | translate }} + + - - - - -
{{ rowData[col.field] }}
- - {{ rowData[col.field] | date: dateFormat }} - - - {{ 'ENUMS.ANNOUNCEMENT_' + col.header + '.' + rowData[col.field] | translate }} - + {{ col.translationPrefix + '.' + col.header | translate }} + + + + +
- - + + + + + + + + + + + + - - - + + +
- + > + + + + +
+ {{ rowData[col.field] }} +
+ + {{ rowData[col.field] | date: dateFormat }} + + + {{ 'ENUMS.ANNOUNCEMENT_' + col.header + '.' + rowData[col.field] | translate }} + - - {{ getDisplayNameWorkspace(rowData[col.field], allMetaData.allWorkspaces) }} - - - {{ getTranslationKeyForNonExistingWorkspaces(rowData.workspaceName) | translate }} - - - {{ 'ANNOUNCEMENT.EVERY_PRODUCT' | translate }} - - - {{ getDisplayNameProduct(rowData[col.field], allMetaData.allProducts) }} - - - - - + + + + + + + + + {{ getDisplayNameWorkspace(rowData[col.field], metaData.allWorkspaces) }} + + + {{ getTranslationKeyForNonExistingWorkspaces(rowData.workspaceName) | translate }} + + + {{ 'ANNOUNCEMENT.EVERY_PRODUCT' | translate }} + + + {{ getDisplayNameProduct(rowData[col.field], metaData.allProducts) }} + + + + + + + + +
+
+
+
+ {{ 'ACTIONS.DELETE.MESSAGE.TEXT' | translate }} +
+
+ {{ limitText(item4Delete?.title, 50) }} +
+
{{ 'ACTIONS.DELETE.MESSAGE.INFO' | translate }}
+
+
+ +
+ + +
+
+
+ - - - -
-
-
-
- {{ 'ACTIONS.DELETE.MESSAGE_TEXT' | translate }} -
-
- {{ limitText(announcement?.title, 50) }} -
-
{{ 'ACTIONS.DELETE.MESSAGE_INFO' | translate }}
-
-
- -
- - -
-
-
diff --git a/src/app/announcement/announcement-search/announcement-search.component.scss b/src/app/announcement/announcement-search/announcement-search.component.scss index ced52f1..a404d82 100644 --- a/src/app/announcement/announcement-search/announcement-search.component.scss +++ b/src/app/announcement/announcement-search/announcement-search.component.scss @@ -29,8 +29,7 @@ .max-height-for-2-lines { max-height: 2.5rem; } - - .announcement-title { + .text-ellipsis-2-lines { @include displaying-text-ellipsis(2); } } diff --git a/src/app/announcement/announcement-search/announcement-search.component.spec.ts b/src/app/announcement/announcement-search/announcement-search.component.spec.ts index f0f12a4..2c4ad53 100644 --- a/src/app/announcement/announcement-search/announcement-search.component.spec.ts +++ b/src/app/announcement/announcement-search/announcement-search.component.spec.ts @@ -1,23 +1,17 @@ import { NO_ERRORS_SCHEMA } from '@angular/core' -import { HttpClient } from '@angular/common/http' -import { provideHttpClient } from '@angular/common/http' -import { provideHttpClientTesting } from '@angular/common/http/testing' import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' +import { provideHttpClient, HttpClient } from '@angular/common/http' +import { provideHttpClientTesting } from '@angular/common/http/testing' import { TranslateLoader, TranslateModule } from '@ngx-translate/core' import { of, throwError } from 'rxjs' -import { - AppStateService, - Column, - createTranslateLoader, - PortalMessageService, - UserService -} from '@onecx/portal-integration-angular' +import { AppStateService, UserService } from '@onecx/angular-integration-interface' +import { Column, createTranslateLoader, PortalMessageService } from '@onecx/portal-integration-angular' import { AnnouncementAssignments, AnnouncementInternalAPIService } from 'src/app/shared/generated' import { AnnouncementSearchComponent } from './announcement-search.component' -const announcementData: any = [ +const itemData: any = [ { modificationCount: 0, id: '9abc8923-6200-4346-858e-cac3ce62e1a6', @@ -63,6 +57,8 @@ describe('AnnouncementSearchComponent', () => { let component: AnnouncementSearchComponent let fixture: ComponentFixture + const mockUserService = { lang$: { getValue: jasmine.createSpy('getValue') } } + const translateServiceSpy = jasmine.createSpyObj('TranslateService', ['get']) const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error', 'info']) const apiServiceSpy = { searchAnnouncements: jasmine.createSpy('searchAnnouncements').and.returnValue(of({})), @@ -71,13 +67,6 @@ describe('AnnouncementSearchComponent', () => { getAllWorkspaceNames: jasmine.createSpy('getAllWorkspaceNames').and.returnValue(of([])), getAllProductNames: jasmine.createSpy('getAllProductNames').and.returnValue(of([])) } - const translateServiceSpy = jasmine.createSpyObj('TranslateService', ['get']) - - const mockUserService = { - lang$: { - getValue: jasmine.createSpy('getValue') - } - } beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -96,9 +85,9 @@ describe('AnnouncementSearchComponent', () => { providers: [ provideHttpClient(), provideHttpClientTesting(), + { provide: UserService, useValue: mockUserService }, { provide: PortalMessageService, useValue: msgServiceSpy }, - { provide: AnnouncementInternalAPIService, useValue: apiServiceSpy }, - { provide: UserService, useValue: mockUserService } + { provide: AnnouncementInternalAPIService, useValue: apiServiceSpy } ] }).compileComponents() msgServiceSpy.success.calls.reset() @@ -126,33 +115,27 @@ describe('AnnouncementSearchComponent', () => { fixture.detectChanges() }) - it('should create', () => { - expect(component).toBeTruthy() - }) - - it('should call search OnInit and populate filteredColumns/actions correctly', () => { - component.columns = [ - { field: 'title', header: 'TITLE', active: false }, - { field: 'workspaceName', header: 'WORKSPACE', active: true } - ] + describe('construction', () => { + it('should create', () => { + expect(component).toBeTruthy() + }) - component.ngOnInit() + it('should call OnInit and populate filteredColumns/actions correctly', () => { + component.ngOnInit() - expect(component.filteredColumns[0].field).toEqual('workspaceName') + expect(component.filteredColumns[0]).toEqual(component.columns[0]) + }) }) describe('search', () => { it('should search announcements without search criteria', (done) => { - apiServiceSpy.searchAnnouncements.and.returnValue(of({ stream: announcementData })) + apiServiceSpy.searchAnnouncements.and.returnValue(of({ stream: itemData })) component.onSearch({ announcementSearchCriteria: {} }) - component.announcements$!.subscribe({ + component.data$!.subscribe({ next: (data) => { - expect(data.length).toBe(3) - expect(data[0]).toEqual(announcementData[0]) - expect(data[1]).toEqual(announcementData[1]) - expect(data[2]).toEqual(announcementData[2]) + expect(data).toEqual(itemData) done() }, error: done.fail @@ -160,35 +143,15 @@ describe('AnnouncementSearchComponent', () => { }) it('should search announcements assigned to one workspace', (done) => { - apiServiceSpy.searchAnnouncements.and.returnValue(of({ stream: [announcementData[1]] })) + apiServiceSpy.searchAnnouncements.and.returnValue(of({ stream: [itemData[1]] })) component.criteria = { workspaceName: 'ADMIN' } - const reuseCriteria = false - component.onSearch({ announcementSearchCriteria: component.criteria }, reuseCriteria) + component.onSearch({ announcementSearchCriteria: component.criteria }, false) - component.announcements$!.subscribe({ + component.data$!.subscribe({ next: (data) => { expect(data.length).toBe(1) - expect(data[0]).toEqual(announcementData[1]) - done() - }, - error: done.fail - }) - }) - - it('should search announcements for all workspaces and products', (done) => { - apiServiceSpy.searchAnnouncements.and.returnValue(of({ stream: announcementData })) - component.criteria = {} - const reuseCriteria = false - - component.onSearch({ announcementSearchCriteria: component.criteria }, reuseCriteria) - - component.announcements$!.subscribe({ - next: (data) => { - expect(data.length).toBe(3) - expect(data[0]).toEqual(announcementData[0]) - expect(data[1]).toEqual(announcementData[1]) - expect(data[2]).toEqual(announcementData[2]) + expect(data[0]).toEqual(itemData[1]) done() }, error: done.fail @@ -196,16 +159,15 @@ describe('AnnouncementSearchComponent', () => { }) it('should reset search criteria and empty announcements for the next search', (done) => { - apiServiceSpy.searchAnnouncements.and.returnValue(of({ stream: [announcementData[1]] })) + apiServiceSpy.searchAnnouncements.and.returnValue(of({ stream: [itemData[1]] })) component.criteria = { workspaceName: 'ADMIN' } - const reuseCriteria = false - component.onSearch({ announcementSearchCriteria: component.criteria }, reuseCriteria) + component.onSearch({ announcementSearchCriteria: component.criteria }, true) - component.announcements$!.subscribe({ + component.data$!.subscribe({ next: (data) => { expect(data.length).toBe(1) - expect(data[0]).toEqual(announcementData[1]) + expect(data[0]).toEqual(itemData[1]) done() }, error: done.fail @@ -216,13 +178,13 @@ describe('AnnouncementSearchComponent', () => { expect(component.criteria).toEqual({}) }) - it('should display an error message if the search call fails', (done) => { + it('should display an error message if the search fails', (done) => { const errorResponse = { status: '403', statusText: 'Not authorized' } apiServiceSpy.searchAnnouncements.and.returnValue(throwError(() => errorResponse)) spyOn(console, 'error') component.onSearch({ announcementSearchCriteria: {} }) - component.announcements$!.subscribe({ + component.data$!.subscribe({ next: (data) => { expect(data.length).toBe(0) done() @@ -231,7 +193,7 @@ describe('AnnouncementSearchComponent', () => { expect(console.error).toHaveBeenCalledWith('searchAnnouncements', errorResponse) expect(component.exceptionKey).toEqual('EXCEPTIONS.HTTP_STATUS_' + errorResponse.status + '.ANNOUNCEMENTS') expect(msgServiceSpy.error).toHaveBeenCalledWith({ - summaryKey: 'ACTIONS.SEARCH.SEARCH_FAILED', + summaryKey: 'ACTIONS.SEARCH.MESSAGE.SEARCH_FAILED', detailKey: 'EXCEPTIONS.HTTP_STATUS_' + errorResponse.status + '.ANNOUNCEMENTS' }) done.fail @@ -251,15 +213,15 @@ describe('AnnouncementSearchComponent', () => { }) it('should search with wildcard * in title', (done) => { - apiServiceSpy.searchAnnouncements.and.returnValue(of({ stream: announcementData })) + apiServiceSpy.searchAnnouncements.and.returnValue(of({ stream: itemData })) component.criteria = { title: 'A*' } const reuseCriteria = false component.onSearch({ announcementSearchCriteria: component.criteria }, reuseCriteria) - component.announcements$!.subscribe({ + component.data$!.subscribe({ next: (data) => { - expect(data).toEqual(announcementData) + expect(data).toEqual(itemData) done() }, error: done.fail @@ -267,120 +229,8 @@ describe('AnnouncementSearchComponent', () => { }) }) - /* - * UI ACTIONS - */ - it('should prepare the creation of a new announcement', () => { - component.onCreate() - - expect(component.changeMode).toEqual('CREATE') - expect(component.announcement).toBe(undefined) - expect(component.displayDetailDialog).toBeTrue() - }) - - it('should show details of an announcement', () => { - const ev: MouseEvent = new MouseEvent('type') - const mode = 'EDIT' - - component.onDetail(ev, announcementData[0], mode) - - expect(component.changeMode).toEqual(mode) - expect(component.announcement).toEqual(announcementData[0]) - expect(component.displayDetailDialog).toBeTrue() - }) - - it('should prepare the copy of an announcement', () => { - const ev: MouseEvent = new MouseEvent('type') - - component.onDetail(ev, announcementData[0], 'COPY') - - expect(component.changeMode).toEqual('CREATE') - expect(component.announcement).toEqual({ ...announcementData[0], id: undefined }) - expect(component.displayDetailDialog).toBeTrue() - }) - - it('should prepare the deletion of an announcement', () => { - const ev: MouseEvent = new MouseEvent('type') - spyOn(ev, 'stopPropagation') - - component.onDelete(ev, announcementData[0]) - - expect(ev.stopPropagation).toHaveBeenCalled() - expect(component.announcement).toBe(announcementData[0]) - expect(component.displayDeleteDialog).toBeTrue() - }) - - it('should delete an announcement item with and without workspace assignment', () => { - const ev: MouseEvent = new MouseEvent('type') - apiServiceSpy.deleteAnnouncementById.and.returnValue(of({})) - const announcements = [ - { id: 'a1', title: 'a1' }, - { id: 'a2', title: 'a2', workspaceName: 'workspace' } - ] - component.onDelete(ev, announcements[0]) - component.onDeleteConfirmation() - - expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.DELETE.MESSAGE.OK' }) - }) - - it('should display error if deleting an announcement fails', () => { - const errorResponse = { status: '400', statusText: 'Deletion failed' } - apiServiceSpy.deleteAnnouncementById.and.returnValue(throwError(() => errorResponse)) - component.announcement = { id: 'definedHere' } - spyOn(console, 'error') - - component.onDeleteConfirmation() - - expect(console.error).toHaveBeenCalledWith('deleteAnnouncementById', errorResponse) - expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.DELETE.MESSAGE.NOK' }) - }) - - it('should set correct values when detail dialog is closed', () => { - spyOn(component, 'onSearch') - - component.onCloseDetail(true) - - expect(component.onSearch).toHaveBeenCalled() - expect(component.displayDeleteDialog).toBeFalse() - }) - - it('should update the columns that are seen in results', () => { - const columns: Column[] = [ - { field: 'workspaceName', header: 'WORKSPACE' }, - { field: 'context', header: 'CONTEXT' } - ] - const expectedColumn = { field: 'workspaceName', header: 'WORKSPACE' } - component.columns = columns - - component.onColumnsChange(['workspaceName']) - - expect(component.filteredColumns).not.toContain(columns[1]) - expect(component.filteredColumns).toEqual([jasmine.objectContaining(expectedColumn)]) - }) - - it('should apply a filter to the result table', () => { - component.announcementTable = jasmine.createSpyObj('announcementTable', ['filterGlobal']) - - component.onFilterChange('test') - - expect(component.announcementTable?.filterGlobal).toHaveBeenCalledWith('test', 'contains') - }) - - it('should open create dialog', () => { - translateServiceSpy.get.and.returnValue(of({ 'ACTIONS.CREATE.LABEL': 'Create' })) - spyOn(component, 'onCreate') - - component.ngOnInit() - - component.actions$?.subscribe((action) => { - action[0].actionCallback() - }) - - expect(component.onCreate).toHaveBeenCalled() - }) - /** - * META data: used products/workspaces (which were assigned to announcements) + * META data: which were assigned to announcements */ describe('META data: load used products/workspaces', () => { it('should get all announcements assigned to workspaces', (done) => { @@ -389,7 +239,7 @@ describe('AnnouncementSearchComponent', () => { component.ngOnInit() - component.allUsedLists$?.subscribe({ + component.usedLists$?.subscribe({ next: (data) => { expect(data.workspaces).toContain({ label: 'w1', value: 'w1' }) expect(data.products).toContain({ label: 'prod1', value: 'prod1' }) @@ -405,7 +255,7 @@ describe('AnnouncementSearchComponent', () => { component.ngOnInit() - component.allUsedLists$?.subscribe({ + component.usedLists$?.subscribe({ next: (data) => { expect(data.workspaces).toEqual([]) expect(data.products).toEqual([]) @@ -528,7 +378,7 @@ describe('AnnouncementSearchComponent', () => { component.ngOnInit() - component.allMetaData$.subscribe({ + component.metaData$.subscribe({ next: (meta) => { if (meta) { expect(meta.allProducts.length).toBe(1) @@ -550,6 +400,147 @@ describe('AnnouncementSearchComponent', () => { }) }) + /* + * UI ACTIONS + */ + describe('detail actions', () => { + it('should prepare the creation of a new parameter', () => { + const ev: MouseEvent = new MouseEvent('type') + spyOn(ev, 'stopPropagation') + const mode = 'CREATE' + + component.onDetail(mode, undefined, ev) + + expect(ev.stopPropagation).toHaveBeenCalled() + expect(component.changeMode).toEqual(mode) + expect(component.item4Detail).toBe(undefined) + expect(component.displayDetailDialog).toBeTrue() + + component.onCloseDetail(false) + + expect(component.displayDetailDialog).toBeFalse() + }) + + it('should show details of a parameter', () => { + const mode = 'EDIT' + + component.onDetail(mode, itemData[0]) + + expect(component.changeMode).toEqual(mode) + expect(component.item4Detail).toBe(itemData[0]) + expect(component.displayDetailDialog).toBeTrue() + }) + + it('should prepare the copy of a parameter', () => { + const mode = 'COPY' + + component.onDetail(mode, itemData[0]) + + expect(component.changeMode).toEqual(mode) + expect(component.item4Detail).toBe(itemData[0]) + expect(component.displayDetailDialog).toBeTrue() + + component.onCloseDetail(true) + + expect(component.displayDetailDialog).toBeFalse() + }) + }) + + describe('deletion', () => { + let items4Deletion: any[] = [] + + beforeEach(() => { + items4Deletion = [ + { id: 'id1', title: 't1', content: 'text1' }, + { id: 'id2', title: 't2', content: 'text2', productName: 'p1' }, + { id: 'id3', title: 't3', content: 'text3', workspace: 'wsp' } + ] + }) + + it('should prepare the deletion of a parameter - ok', () => { + const ev: MouseEvent = new MouseEvent('type') + spyOn(ev, 'stopPropagation') + + component.onDelete(ev, items4Deletion[0]) + + expect(ev.stopPropagation).toHaveBeenCalled() + expect(component.item4Delete).toBe(items4Deletion[0]) + expect(component.displayDeleteDialog).toBeTrue() + }) + + it('should delete a parameter with confirmation', () => { + apiServiceSpy.deleteAnnouncementById.and.returnValue(of(null)) + const ev: MouseEvent = new MouseEvent('type') + + component.onDelete(ev, items4Deletion[1]) + component.onDeleteConfirmation(items4Deletion) // remove but not the last of the product + + expect(component.displayDeleteDialog).toBeFalse() + expect(msgServiceSpy.success).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.DELETE.MESSAGE.OK' }) + + component.onDelete(ev, items4Deletion[2]) + component.onDeleteConfirmation(items4Deletion) // remove and this was the last of the product + }) + + it('should display error if deleting a parameter fails', () => { + const errorResponse = { status: '400', statusText: 'Error on deletion' } + apiServiceSpy.deleteAnnouncementById.and.returnValue(throwError(() => errorResponse)) + const ev: MouseEvent = new MouseEvent('type') + spyOn(console, 'error') + + component.onDelete(ev, items4Deletion[0]) + component.onDeleteConfirmation(items4Deletion) + + expect(msgServiceSpy.error).toHaveBeenCalledWith({ summaryKey: 'ACTIONS.DELETE.MESSAGE.NOK' }) + expect(console.error).toHaveBeenCalledWith('deleteAnnouncementById', errorResponse) + }) + + it('should reject confirmation if param was not set', () => { + component.onDeleteConfirmation(items4Deletion) + + expect(apiServiceSpy.deleteAnnouncementById).not.toHaveBeenCalled() + }) + }) + + describe('filter columns', () => { + it('should update the columns that are seen in results', () => { + const columns: Column[] = [ + { field: 'workspaceName', header: 'WORKSPACE' }, + { field: 'context', header: 'CONTEXT' } + ] + const expectedColumn = { field: 'workspaceName', header: 'WORKSPACE' } + component.columns = columns + + component.onColumnsChange(['workspaceName']) + + expect(component.filteredColumns).not.toContain(columns[1]) + expect(component.filteredColumns).toEqual([jasmine.objectContaining(expectedColumn)]) + }) + + it('should apply a filter to the result table', () => { + component.dataTable = jasmine.createSpyObj('dataTable', ['filterGlobal']) + + component.onFilterChange('test') + + expect(component.dataTable?.filterGlobal).toHaveBeenCalledWith('test', 'contains') + }) + }) + + describe('action buttons', () => { + it('should open create dialog', () => { + translateServiceSpy.get.and.returnValue(of({ 'ACTIONS.CREATE.LABEL': 'Create' })) + spyOn(component, 'onDetail') + + component.ngOnInit() + + component.actions$?.subscribe((action) => { + action[0].actionCallback() + }) + + expect(component.onDetail).toHaveBeenCalled() + }) + }) + /** * Language tests */ diff --git a/src/app/announcement/announcement-search/announcement-search.component.ts b/src/app/announcement/announcement-search/announcement-search.component.ts index 43c93f0..3e2c83e 100644 --- a/src/app/announcement/announcement-search/announcement-search.component.ts +++ b/src/app/announcement/announcement-search/announcement-search.component.ts @@ -4,15 +4,15 @@ import { catchError, combineLatest, finalize, map, Observable, of } from 'rxjs' import { Table } from 'primeng/table' import { SelectItem } from 'primeng/api' -import { Action, Column, PortalMessageService, UserService } from '@onecx/portal-integration-angular' +import { UserService } from '@onecx/angular-integration-interface' +import { Action, Column, DataViewControlTranslations, PortalMessageService } from '@onecx/portal-integration-angular' import { Announcement, AnnouncementAssignments, AnnouncementInternalAPIService, AnnouncementSearchCriteria, SearchAnnouncementsRequestParams, - WorkspaceAbstract, - ProductsPageResult + WorkspaceAbstract } from 'src/app/shared/generated' import { limitText, dropDownSortItemsByLabel } from 'src/app/shared/utils' @@ -20,10 +20,9 @@ export type ChangeMode = 'VIEW' | 'COPY' | 'CREATE' | 'EDIT' type ExtendedColumn = Column & { hasFilter?: boolean isDate?: boolean - isDropdown?: true - css?: string + isDropdown?: boolean limit?: boolean - needsDisplayName?: boolean + css?: string } type AllMetaData = { allProducts: SelectItem[] @@ -39,26 +38,30 @@ type AllUsedLists = { products: SelectItem[]; workspaces: SelectItem[] } styleUrls: ['./announcement-search.component.scss'] }) export class AnnouncementSearchComponent implements OnInit { - @ViewChild('announcementTable', { static: false }) announcementTable: Table | undefined - + // dialog public loading = false public searching = false public exceptionKey: string | undefined = undefined - public changeMode: ChangeMode = 'CREATE' + public changeMode: ChangeMode = 'VIEW' public dateFormat: string public actions$: Observable | undefined public criteria: AnnouncementSearchCriteria = {} - public announcement: Announcement | undefined - public announcements$: Observable | undefined - public displayDeleteDialog = false public displayDetailDialog = false + public displayDeleteDialog = false public filteredColumns: Column[] = [] public limitText = limitText - public allMetaData$!: Observable // collection of data used in UI + @ViewChild('dataTable', { static: false }) dataTable: Table | undefined + public dataViewControlsTranslations: DataViewControlTranslations = {} + + // data + public data$: Observable | undefined + public metaData$!: Observable // collection of data used in UI public allWorkspaces$!: Observable // getting data from bff endpoint public allProducts$!: Observable // getting data from bff endpoint - public allUsedLists$!: Observable // getting data from bff endpoint + public usedLists$!: Observable // getting data from bff endpoint + public item4Detail: Announcement | undefined // used on detail + public item4Delete: Announcement | undefined // used on deletion public columns: ExtendedColumn[] = [ { @@ -80,16 +83,14 @@ export class AnnouncementSearchComponent implements OnInit { header: 'WORKSPACE', active: true, translationPrefix: 'ANNOUNCEMENT', - css: 'text-center', - needsDisplayName: true + css: 'text-center' }, { field: 'productName', header: 'PRODUCT_NAME', active: true, translationPrefix: 'ANNOUNCEMENT', - css: 'text-center', - needsDisplayName: true + css: 'text-center' }, { field: 'type', @@ -128,31 +129,57 @@ export class AnnouncementSearchComponent implements OnInit { constructor( private readonly user: UserService, - private readonly announcementApi: AnnouncementInternalAPIService, private readonly msgService: PortalMessageService, - private readonly translate: TranslateService + private readonly translate: TranslateService, + private readonly announcementApi: AnnouncementInternalAPIService ) { this.dateFormat = this.user.lang$.getValue() === 'de' ? 'dd.MM.yyyy HH:mm' : 'M/d/yy, h:mm a' + this.filteredColumns = this.columns.filter((a) => a.active === true) } - ngOnInit(): void { - this.loading = true + public ngOnInit(): void { this.prepareDataLoad() this.loadData() + this.prepareDialogTranslations() this.prepareActionButtons() - this.filteredColumns = this.columns.filter((a) => { - return a.active === true - }) } + /** + * Dialog preparation + */ + private prepareDialogTranslations(): void { + this.translate + .get([ + 'DIALOG.DATAVIEW.FILTER', + 'DIALOG.DATAVIEW.FILTER_BY', + 'ANNOUNCEMENT.TITLE', + 'ANNOUNCEMENT.WORKSPACE', + 'ANNOUNCEMENT.PRODUCT_NAME' + ]) + .pipe( + map((data) => { + this.dataViewControlsTranslations = { + filterInputPlaceholder: data['DIALOG.DATAVIEW.FILTER'], + filterInputTooltip: + data['DIALOG.DATAVIEW.FILTER_BY'] + + data['ANNOUNCEMENT.TITLE'] + + ', ' + + data['ANNOUNCEMENT.WORKSPACE'] + + ', ' + + data['ANNOUNCEMENT.PRODUCT_NAME'] + } + }) + ) + .subscribe() + } private prepareActionButtons(): void { - this.actions$ = this.translate.get(['ACTIONS.CREATE.LABEL', 'ACTIONS.CREATE.ANNOUNCEMENT.TOOLTIP']).pipe( + this.actions$ = this.translate.get(['ACTIONS.CREATE.LABEL', 'ACTIONS.CREATE.TOOLTIP']).pipe( map((data) => { return [ { label: data['ACTIONS.CREATE.LABEL'], - title: data['ACTIONS.CREATE.ANNOUNCEMENT.TOOLTIP'], - actionCallback: () => this.onCreate(), + title: data['ACTIONS.CREATE.TOOLTIP'], + actionCallback: () => this.onDetail('CREATE', undefined), icon: 'pi pi-plus', show: 'always', permission: 'ANNOUNCEMENT#EDIT' @@ -163,86 +190,59 @@ export class AnnouncementSearchComponent implements OnInit { } /**************************************************************************** - * SEARCH announcements + * UI Events */ - public onSearch(criteria: SearchAnnouncementsRequestParams, reuseCriteria = false): void { - if (!reuseCriteria) { - if (criteria.announcementSearchCriteria.workspaceName === '') - criteria.announcementSearchCriteria.workspaceName = undefined - if (criteria.announcementSearchCriteria.productName === '') - criteria.announcementSearchCriteria.productName = undefined - this.criteria = criteria.announcementSearchCriteria - } - this.searching = true - this.exceptionKey = undefined - this.announcements$ = this.announcementApi.searchAnnouncements(criteria).pipe( - map((data) => data.stream ?? []), - catchError((err) => { - this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_' + err.status + '.ANNOUNCEMENTS' - this.msgService.error({ summaryKey: 'ACTIONS.SEARCH.MSG_SEARCH_FAILED' }) - console.error('searchAnnouncements', err) - return of([] as Announcement[]) - }), - finalize(() => (this.searching = false)) - ) - } - public onCriteriaReset(): void { this.criteria = {} } - public onColumnsChange(activeIds: string[]) { this.filteredColumns = activeIds.map((id) => this.columns.find((col) => col.field === id)) as Column[] } public onFilterChange(event: string): void { - this.announcementTable?.filterGlobal(event, 'contains') + this.dataTable?.filterGlobal(event, 'contains') } - /**************************************************************************** - * CHANGES - */ + // Detail => CREATE, COPY, EDIT, VIEW + public onDetail(mode: ChangeMode, item: Announcement | undefined, ev?: Event): void { + ev?.stopPropagation() + this.changeMode = mode + this.item4Detail = item // do not manipulate the item here + this.displayDetailDialog = true + } public onCloseDetail(refresh: boolean): void { this.displayDetailDialog = false - this.displayDeleteDialog = false - this.announcement = undefined + this.item4Detail = undefined if (refresh) { - //this.getUsedWorkspacesAndProducts() - this.onSearch({ announcementSearchCriteria: {} }, true) + this.loadData() } } - public onCreate() { - this.changeMode = 'CREATE' - this.announcement = undefined - this.displayDetailDialog = true - } - public onDetail(ev: MouseEvent, item: Announcement, mode: ChangeMode): void { - ev.stopPropagation() - this.changeMode = mode === 'COPY' ? 'CREATE' : mode - this.announcement = { ...item, id: ['COPY', 'CREATE'].includes(mode) ? undefined : item.id } - this.displayDetailDialog = true - } - - public onDelete(ev: MouseEvent, item: Announcement): void { + // DELETE => Ask for confirmation + public onDelete(ev: Event, item: Announcement): void { ev.stopPropagation() - this.announcement = item + this.item4Delete = item this.displayDeleteDialog = true } - public onDeleteConfirmation(): void { - if (this.announcement?.id) { - //const workspaceUsed = this.announcement?.workspaceName !== undefined - this.announcementApi.deleteAnnouncementById({ id: this.announcement?.id }).subscribe({ - next: () => { - this.onCloseDetail(true) - //this.announcementTable?._value = this.announcementTable?._value.filter((a) => a.id !== this.announcement?.id) - this.msgService.success({ summaryKey: 'ACTIONS.DELETE.MESSAGE.OK' }) - }, - error: (err) => { - console.error('deleteAnnouncementById', err) - this.msgService.error({ summaryKey: 'ACTIONS.DELETE.MESSAGE.NOK' }) - } - }) - } + // user confirmed deletion + public onDeleteConfirmation(data: Announcement[]): void { + if (!this.item4Delete?.id) return + this.announcementApi.deleteAnnouncementById({ id: this.item4Delete?.id }).subscribe({ + next: () => { + this.msgService.success({ summaryKey: 'ACTIONS.DELETE.MESSAGE.OK' }) + // remove item from data + data = data?.filter((d) => d.id !== this.item4Delete?.id) + // check remaing data if product still exists - if not then reload + const d = data?.filter((d) => d.productName === this.item4Delete?.productName) + this.item4Delete = undefined + this.displayDeleteDialog = false + if (d?.length === 0) this.loadData() + else this.onSearch({ announcementSearchCriteria: {} }, true) + }, + error: (err) => { + this.msgService.error({ summaryKey: 'ACTIONS.DELETE.MESSAGE.NOK' }) + console.error('deleteAnnouncementById', err) + } + }) } // workspace in list of all workspaces? @@ -264,13 +264,14 @@ export class AnnouncementSearchComponent implements OnInit { } /**************************************************************************** - * SEARCHING of META DATA - * used to display readable names in drop down lists and result set + * SEARCHING + * 1. Loading META DATA used to display drop down lists => products, workspaces + * 2. Trigger searching data */ private prepareDataLoad(): void { - // declare search for ALL products privided by bff + // declare search for ALL products provided by bff this.allProducts$ = this.announcementApi.getAllProductNames({ productsSearchCriteria: {} }).pipe( - map((data: ProductsPageResult) => { + map((data) => { const si: SelectItem[] = [] if (data.stream) { for (const product of data.stream) { @@ -296,13 +297,14 @@ export class AnnouncementSearchComponent implements OnInit { return si }), catchError((err) => { + this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_' + err.status + '.WORKSPACES' console.error('getAllWorkspaceNames', err) return of([] as SelectItem[]) }) ) // declare search for used products/workspaces (used === assigned to announcement) // hereby SelectItem[] are prepared but with a temporary label (=> displayName) - this.allUsedLists$ = this.announcementApi.getAllAnnouncementAssignments().pipe( + this.usedLists$ = this.announcementApi.getAllAnnouncementAssignments().pipe( map((data: AnnouncementAssignments) => { const ul: AllUsedLists = { products: [], workspaces: [] } if (data.productNames) @@ -320,8 +322,9 @@ export class AnnouncementSearchComponent implements OnInit { } private loadData(): void { + this.loading = true const allDataLists$ = combineLatest([this.allWorkspaces$, this.allProducts$]) - this.allMetaData$ = combineLatest([allDataLists$, this.allUsedLists$]).pipe( + this.metaData$ = combineLatest([allDataLists$, this.usedLists$]).pipe( map(([[aW, aP], aul]: [[SelectItem[], SelectItem[]], AllUsedLists]) => { // enrich the temporary prepared lists with display names contained in allLists aul.products.forEach((p) => { @@ -330,10 +333,34 @@ export class AnnouncementSearchComponent implements OnInit { aul.workspaces.forEach((w) => { w.label = this.getDisplayNameWorkspace(w.value, aW) }) - this.loading = false - this.onSearch({ announcementSearchCriteria: {} }) + if (!this.exceptionKey) this.onSearch({ announcementSearchCriteria: {} }) return { allProducts: aP, allWorkspaces: aW, usedProducts: aul.products, usedWorkspaces: aul.workspaces } - }) + }), + finalize(() => (this.loading = false)) + ) + } + /**************************************************************************** + * SEARCH announcements + */ + public onSearch(criteria: SearchAnnouncementsRequestParams, reuseCriteria = false): void { + if (!reuseCriteria) { + if (criteria.announcementSearchCriteria.workspaceName === '') + criteria.announcementSearchCriteria.workspaceName = undefined + if (criteria.announcementSearchCriteria.productName === '') + criteria.announcementSearchCriteria.productName = undefined + this.criteria = criteria.announcementSearchCriteria + } + this.searching = true + this.exceptionKey = undefined + this.data$ = this.announcementApi.searchAnnouncements(criteria).pipe( + map((data) => data.stream ?? []), + catchError((err) => { + this.exceptionKey = 'EXCEPTIONS.HTTP_STATUS_' + err.status + '.ANNOUNCEMENTS' + this.msgService.error({ summaryKey: 'ACTIONS.SEARCH.MESSAGE.SEARCH_FAILED' }) + console.error('searchAnnouncements', err) + return of([] as Announcement[]) + }), + finalize(() => (this.searching = false)) ) } } diff --git a/src/app/announcement/announcement.module.ts b/src/app/announcement/announcement.module.ts index 9230448..accdcc2 100644 --- a/src/app/announcement/announcement.module.ts +++ b/src/app/announcement/announcement.module.ts @@ -1,12 +1,12 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' -import { FormsModule } from '@angular/forms' import { RouterModule, Routes } from '@angular/router' -import { PortalCoreModule } from '@onecx/portal-integration-angular' import { InitializeModuleGuard, addInitializeModuleGuard } from '@onecx/angular-integration-interface' +import { PortalCoreModule } from '@onecx/portal-integration-angular' + +import { SharedModule } from 'src/app/shared/shared.module' -import { SharedModule } from '../shared/shared.module' import { AnnouncementSearchComponent } from './announcement-search/announcement-search.component' import { AnnouncementCriteriaComponent } from './announcement-search/announcement-criteria/announcement-criteria.component' import { AnnouncementDetailComponent } from './announcement-detail/announcement-detail.component' @@ -28,7 +28,6 @@ const routes: Routes = [ declarations: [AnnouncementSearchComponent, AnnouncementDetailComponent, AnnouncementCriteriaComponent], imports: [ CommonModule, - FormsModule, PortalCoreModule.forMicroFrontend(), [RouterModule.forChild(addInitializeModuleGuard(routes))], SharedModule diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 6a9a60a..4a00f45 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -2,9 +2,10 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { TranslateModule, TranslateService } from '@ngx-translate/core' -import { provideErrorTailorConfig, errorTailorImports } from '@ngneat/error-tailor' +import { provideErrorTailorConfig, errorTailorImports, DefaultControlErrorComponent } from '@ngneat/error-tailor' import { AutoCompleteModule } from 'primeng/autocomplete' + import { CalendarModule } from 'primeng/calendar' import { ConfirmDialogModule } from 'primeng/confirmdialog' import { ConfirmPopupModule } from 'primeng/confirmpopup' @@ -98,6 +99,7 @@ import { LabelResolver } from './label.resolver' useFactory: (i18n: TranslateService) => { return { required: () => i18n.instant('VALIDATION.ERRORS.EMPTY_REQUIRED_FIELD'), + //required: 'henry', maxlength: ({ requiredLength }) => i18n.instant('VALIDATION.ERRORS.MAXIMUM_LENGTH').replace('{{chars}}', requiredLength), minlength: ({ requiredLength }) => @@ -107,6 +109,7 @@ import { LabelResolver } from './label.resolver' }, deps: [TranslateService] }, + controlErrorComponent: DefaultControlErrorComponent, //this is required because primeng calendar wraps things in an ugly way blurPredicate: (element: Element) => { return ['INPUT', 'TEXTAREA', 'SELECT', 'CUSTOM-DATE', 'P-CALENDAR', 'P-DROPDOWN'].some( diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 076ce50..bd394b6 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -1,5 +1,7 @@ { "ACTIONS": { + "LABEL": "Aktionen", + "TOOLTIP": "Aktionen für die Daten der Taellenzeile", "CONFIRMATION": { "NO": "Nein", "YES": "Ja", @@ -8,56 +10,71 @@ }, "COPY": { "LABEL": "Kopieren", - "ANNOUNCEMENT.TOOLTIP": "Mit den Daten eine neue Neuigkeit anlegen", + "TOOLTIP": "Mit den Daten eine neue Neuigkeit anlegen", "CLIPBOARD": "In die Zwischenablage kopieren" }, "CREATE": { "LABEL": "Erstellen", - "ANNOUNCEMENT": "Neuigkeit erstellen", - "ANNOUNCEMENT.TOOLTIP": "Eine neue Neuigkeit erstellen", + "TOOLTIP": "Eine neue Neuigkeit erstellen", "MESSAGE": { - "ANNOUNCEMENT_ALREADY_EXIST": "Eine Neuigkeit mit diesem Titel existert schon", "OK": "Die Neuigkeit wurde erfolgreich erstellt", "NOK": "Ein Fehler ist aufgetreten. Die Neuigkeit wurde nicht erstellt." } }, "DELETE": { "LABEL": "Löschen", - "ANNOUNCEMENT": "Neuigkeit löschen", - "ANNOUNCEMENT.TOOLTIP": "Diese Neuigkeit löschen", - "MESSAGE_TEXT": "Möchten Sie diese Neuigkeit löschen?", - "MESSAGE_INFO": "Diese Aktion kann nicht rückgängig gemacht werden!", + "TOOLTIP": "Neuigkeit löschen", "MESSAGE": { + "TEXT": "Möchten Sie diese Neuigkeit löschen?", + "INFO": "Diese Aktion kann nicht rückgängig gemacht werden!", "OK": "Die Neuigkeit wurde erfolgreich gelöscht", "NOK": "Ein Fehler ist aufgetreten. Die Neuigkeit wurde nicht gelöscht." } }, "EDIT": { "LABEL": "Bearbeiten", - "ANNOUNCEMENT": "Neuigkeit bearbeiten", - "ANNOUNCEMENT.TOOLTIP": "Die Neuigkeit bearbeiten", + "TOOLTIP": "Neuigkeit bearbeiten", "MESSAGE": { "OK": "Die Neuigkeit wurde erfolgreich gespeichert", "NOK": "Ein Fehler ist aufgetreten. Die Neuigkeit wurde nicht gespeichert." } }, + "EXPORT": { + "LABEL": "Export", + "TOOLTIP": "Definition als JSON herunterladen", + "MESSAGE": { + "NOK": "Ein Fehler ist aufgetreten. Die Parameter konnten nicht exportiert werden." + } + }, + "IMPORT": { + "LABEL": "Import", + "TOOLTIP": "Definition als JSON hochladen", + "MESSAGE": { + "OK": "Die Parameter wurden erfolgreich importiert.", + "NOK": "Ein Fehler ist aufgetreten. Die Parameter wurden nicht importiert." + } + }, + "NAVIGATION": { + "CLOSE": "Schließen", + "CLOSE.TOOLTIP": "Dialog schließen", + "CLOSE_WITHOUT_SAVE": "Dialog ohne Änderungen schließen", + "BACK": "Zurück", + "NEXT": "Weiter", + "NEXT.TOOLTIP": "Nächste Seite", + "BACK.TOOLTIP": "Vorherige Seite" + }, "SEARCH": { - "ACTIONS": "Aktionen", - "LABEL": "Suchen", - "LABEL.TOOLTIP": "Suche starten", - "RESET": "Zurücksetzen", - "RESET_TOOLTIP": "Suchkriterien löschen", "NO_DATA": "Keine Ergebnisse", - "NO_RESULTS": "Die Suche ergab keine Ergebnisse", "IN_PROGRESS": "Suche...", - "SEARCH_FAILED": "Die Suche ist fehlgeschlagen", - "NOT_FOUND": "Keine Daten gefunden", - "OF": "von" + "OF": "von", + "MESSAGE": { + "NO_RESULTS": "Die Suche ergab keine Treffer", + "SEARCH_FAILED": "Suche ist fehlgeschlagen" + } }, "VIEW": { "LABEL": "Details", - "ANNOUNCEMENT": "Neuigkeit Details", - "ANNOUNCEMENT.TOOLTIP": "Die Details der Neuigkeit anzeigen" + "TOOLTIP": "Neuigkeitdetails" }, "CANCEL": "Abbrechen", "CHOOSE": "Auswählen", @@ -77,11 +94,49 @@ "SAVE_AS": "Speichern als neues {{TYPE}}" } }, + "INTERNAL": { + "CREATION_DATE": "Erstellt am", + "CREATION_USER": "Erstellt von", + "MODIFICATION_DATE": "Geändert am", + "MODIFICATION_USER": "Geändert von", + "OPERATOR": "Automatisiert erstellt", + "TOOLTIPS": { + "CREATION_DATE": "Zeitpunkt der Erstellung", + "CREATION_USER": "Name des Benutzers der Erstellung", + "MODIFICATION_DATE": "Zeitpunkt der letzten Änderung", + "MODIFICATION_USER": "Name des Benutzers der letzten Änderung" + } + }, "DIALOG": { - "DETAIL.ANNOUNCEMENT.PROPERTIES": "Eigenschaften", - "DETAIL.ANNOUNCEMENT.PROPERTIES.TOOLTIP": "Eigenschaften der Neuigkeit", - "SEARCH.HEADER": "Neuigkeiten", - "SEARCH.SUBHEADER": "Suchen und Verwalten von Neuigkeiten" + "SEARCH": { + "HEADER": "Neuigkeiten", + "SUBHEADER": "Suchen und Verwalten von Neuigkeiten" + }, + "DETAIL": { + "COPY.HEADER": "Neuigkeit erstellen", + "CREATE.HEADER": "Neuigkeit erstellen", + "DELETE.HEADER": "Neuigkeit löschen", + "EDIT.HEADER": "Neuigkeit ändern", + "VIEW.HEADER": "Neuigkeit Details", + "TAB": { + "PROPS": "Eigenschaften", + "CONTENT": "Inhalt", + "INTERN": "Intern", + "TOOLTIPS": { + "PROPS": "Basiseigenschaften des Parameters", + "CONTENT": "Titel und Inhalt der Neuigkeit", + "INTERN": "Interne Eigenschaften" + } + }, + "CHARACTERS": "Noch mögliche Zeichen" + }, + "DATAVIEW": { + "FILTER": "Filter", + "FILTER_TOOLTIP": "...filtern", + "FILTER_BY": "Filter für ", + "FILTER_CLEAR": "Filter entfernen", + "FILTER_PLACEHOLDER": "Filter" + } }, "ANNOUNCEMENT": { "ID": "Id", @@ -100,7 +155,7 @@ "ALL": "Alle", "EVERY_WORKSPACE": "Alle", "EVERY_PRODUCT": "Alle", - "WORKSPACE_NOT_FOUND": "-- nicht gefunden --", + "WORKSPACE_NOT_FOUND": "- unbekannt -", "BANNER": { "CLOSE": "Schließen", "WORKSPACE": "Workspace", @@ -164,6 +219,13 @@ "WORKSPACES": "Unbekanntes Problem beim Abrufen von Workspaces - bitte versuchen Sie es noch einmal.", "PRODUCTS": "Unbekanntes Problem beim Abrufen von Applikationen - bitte versuchen Sie es noch einmal." }, + "HTTP_STATUS_400": { + "ANNOUNCEMENT": "Ihre Anfrage konnte nicht bearbeitet werden.", + "ANNOUNCEMENTS": "Ihre Anfrage konnte nicht bearbeitet werden.", + "ASSIGNMENTS": "Ihre Anfrage konnte nicht bearbeitet werden.", + "WORKSPACES": "Ihre Anfrage konnte nicht bearbeitet werden.", + "PRODUCTS": "Ihre Anfrage konnte nicht bearbeitet werden." + }, "HTTP_STATUS_401": { "ANNOUNCEMENT": "Sie sind nicht autorisiert, um Neuigkeiten zu sehen.", "ANNOUNCEMENTS": "Sie sind nicht autorisiert, um Neuigkeiten zu sehen.", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index da91763..97ef825 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1,5 +1,7 @@ { "ACTIONS": { + "LABEL": "Actions", + "TOOLTIP": "Actions for the data of the table row", "CONFIRMATION": { "NO": "No", "YES": "Yes", @@ -8,56 +10,71 @@ }, "COPY": { "LABEL": "Copy", - "ANNOUNCEMENT.TOOLTIP": "Create a new Announcement with the data", + "TOOLTIP": "Create a new Announcement with the data", "CLIPBOARD": "Copy to clipboard" }, "CREATE": { "LABEL": "Create", - "ANNOUNCEMENT": "Create Announcement", - "ANNOUNCEMENT.TOOLTIP": "Create a new Announcement", + "TOOLTIP": "Create a new Announcement", "MESSAGE": { - "ANNOUNCEMENT_ALREADY_EXIST": "An Announcement with that title already exists", "OK": "The Announcement was created successfully", "NOK": "An error has occurred. The Announcement was not created." } }, "DELETE": { "LABEL": "Delete", - "ANNOUNCEMENT": "Delete Announcement", - "ANNOUNCEMENT.TOOLTIP": "Delete this Announcement", - "MESSAGE_TEXT": "Do you want to delete this?", - "MESSAGE_INFO": "This action cannot be undone!", + "TOOLTIP": "Delete Announcement", "MESSAGE": { + "TEXT": "Do you want to delete this Announcement?", + "INFO": "This action cannot be undone!", "OK": "The Announcement was deleted successfully", "NOK": "An error has occurred. The Announcement was not deleted." } }, "EDIT": { "LABEL": "Edit", - "ANNOUNCEMENT": "Edit Announcement", - "ANNOUNCEMENT.TOOLTIP": "Edit the Announcement properties", + "TOOLTIP": "Edit Announcement", "MESSAGE": { "OK": "The Announcement was saved successfully", "NOK": "An error has occurred. The Announcement was not saved." } }, + "EXPORT": { + "LABEL": "Export", + "TOOLTIP": "Export definition in JSON format", + "MESSAGE": { + "NOK": "An error has occurred. The Parameters could not be exported." + } + }, + "IMPORT": { + "LABEL": "Import", + "TOOLTIP": "Upload definition in JSON format", + "MESSAGE": { + "OK": "Die Parameter wurden erfolgreich importiert.", + "NOK": "An error has occurred. The Parameters could not be exported." + } + }, + "NAVIGATION": { + "CLOSE": "Close", + "CLOSE.TOOLTIP": "Close dialog", + "CLOSE_WITHOUT_SAVE": "Close dialog without changes", + "BACK": "Back", + "NEXT": "Next", + "NEXT.TOOLTIP": "Next page", + "BACK.TOOLTIP": "Previous page" + }, "SEARCH": { - "ACTIONS": "Actions", - "LABEL": "Search", - "LABEL.TOOLTIP": "Start Searching", - "RESET": "Reset", - "RESET_TOOLTIP": "Clear given criteria", "NO_DATA": "No data", - "NO_RESULTS": "Search returned no results", "IN_PROGRESS": "Searching...", - "SEARCH_FAILED": "Search failed", - "NOT_FOUND": "No data found", - "OF": "of" + "OF": "of", + "MESSAGE": { + "NO_RESULTS": "Search returned no results", + "SEARCH_FAILED": "Search failed" + } }, "VIEW": { - "LABEL": "Show Details", - "ANNOUNCEMENT": "Announcement Details", - "ANNOUNCEMENT.TOOLTIP": "Show the details of the Announcement" + "LABEL": "Details", + "TOOLTIP": "Announcement Details" }, "CANCEL": "Cancel", "CHOOSE": "Choose", @@ -77,11 +94,49 @@ "SAVE_AS": "Save as new {{TYPE}}" } }, + "INTERNAL": { + "CREATION_DATE": "Created on", + "CREATION_USER": "Created by", + "MODIFICATION_DATE": "Changed on", + "MODIFICATION_USER": "Changed by", + "OPERATOR": "Created by automation", + "TOOLTIPS": { + "CREATION_DATE": "Timestamp of the creation", + "CREATION_USER": "Name of the user of the creation", + "MODIFICATION_DATE": "Timestamp of the last change", + "MODIFICATION_USER": "Name of the user of the last change" + } + }, "DIALOG": { - "DETAIL.ANNOUNCEMENT.PROPERTIES": "Properties", - "DETAIL.ANNOUNCEMENT.PROPERTIES.TOOLTIP": "Properties of the Announcement", - "SEARCH.HEADER": "Announcements", - "SEARCH.SUBHEADER": "Searching and managing of Announcements" + "SEARCH": { + "HEADER": "Announcements", + "SUBHEADER": "Search and manage Announcements" + }, + "DETAIL": { + "COPY.HEADER": "Create Announcement", + "CREATE.HEADER": "Create Announcement", + "DELETE.HEADER": "Delete Announcement", + "EDIT.HEADER": "Edit Announcement", + "VIEW.HEADER": "Announcement Details", + "TAB": { + "PROPS": "Properties", + "CONTENT": "Content", + "INTERN": "Internal", + "TOOLTIPS": { + "PROPS": "Base properties of the Parameter", + "CONTENT": "Titel and Content of the Parameter", + "INTERN": "Internal Properties" + } + }, + "CHARACTERS": "Still remaining characters" + }, + "DATAVIEW": { + "FILTER": "Filter", + "FILTER_TOOLTIP": "Filter Applications", + "FILTER_BY": "Filter by ", + "FILTER_CLEAR": "Clear Filter", + "FILTER_PLACEHOLDER": "Filter" + } }, "ANNOUNCEMENT": { "ID": "Id", @@ -100,7 +155,7 @@ "ALL": "All", "EVERY_WORKSPACE": "All", "EVERY_PRODUCT": "All", - "WORKSPACE_NOT_FOUND": "-- not found --", + "WORKSPACE_NOT_FOUND": "- unknown -", "BANNER": { "CLOSE": "Close", "WORKSPACE": "Workspace", @@ -164,6 +219,13 @@ "WORKSPACES": "Unknown problem retrieving Workspace data - please try again.", "PRODUCTS": "Unknown problem retrieving Application data - please try again." }, + "HTTP_STATUS_400": { + "ANNOUNCEMENT": "Your request could not be processed.", + "ANNOUNCEMENTS": "Your request could not be processed.", + "ASSIGNMENTS": "Your request could not be processed.", + "WORKSPACES": "Your request could not be processed.", + "PRODUCTS": "Your request could not be processed." + }, "HTTP_STATUS_401": { "ANNOUNCEMENT": "You are not authorized to see Announcements.", "ANNOUNCEMENTS": "You are not authorized to see Announcements.", diff --git a/tsconfig.app.json b/tsconfig.app.json index 8284eda..82d03b9 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -14,6 +14,6 @@ "src/app/remotes/announcement-banner/announcement-banner.component.main.ts", "src/app/remotes/announcement-list-active/announcement-list-active.component.main.ts" ], - "include": ["src/**/*.d.ts"], + "include": ["src/**/*.ts"], "exclude": ["src/test.ts", "src/**/*.spec.ts"] } diff --git a/webpack.config.js b/webpack.config.js index 996d2a3..2c624ea 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -18,6 +18,7 @@ const config = withModuleFederationPlugin({ '@angular/platform-browser': { requiredVersion: 'auto', includeSecondaries: true }, '@angular/router': { requiredVersion: 'auto', includeSecondaries: true }, '@ngx-translate/core': { requiredVersion: 'auto' }, + '@ngneat/error-tailor': { requiredVersion: 'auto', includeSecondaries: true }, primeng: { requiredVersion: 'auto', includeSecondaries: true }, rxjs: { requiredVersion: 'auto', includeSecondaries: true }, '@onecx/accelerator': { requiredVersion: 'auto', includeSecondaries: true },