-
Notifications
You must be signed in to change notification settings - Fork 999
WEB-1200: Upload Eclipse BIRT report designs from Manage Reports #3942
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
IOhacker
merged 3 commits into
openMF:dev
from
parth-sharma-10:WEB-1200-upload-birt-report
Sep 2, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| /** | ||
| * Copyright since 2025 Mifos Initiative | ||
| * | ||
| * This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| */ | ||
|
|
||
| import { provideHttpClient } from '@angular/common/http'; | ||
| import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; | ||
| import { Component } from '@angular/core'; | ||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||
|
|
||
| import { AuthenticationService } from '../core/authentication/authentication.service'; | ||
| import { HasPermissionDirective } from '../directives/has-permission/has-permission.directive'; | ||
| import { environment } from '../../environments/environment'; | ||
| import { SystemService } from './system.service'; | ||
| import { | ||
| MAX_REPORT_DESIGN_SIZE_BYTES, | ||
| UploadReportFileDialogComponent | ||
| } from './manage-reports/upload-report-file-dialog/upload-report-file-dialog.component'; | ||
|
|
||
| /** Builds a File of a given size without allocating the bytes twice. */ | ||
| function fileOf(name: string, size: number): File { | ||
| const file = new File(['x'], name); | ||
| Object.defineProperty(file, 'size', { value: size }); | ||
| return file; | ||
| } | ||
|
|
||
| function changeEvent(file: File | null): any { | ||
| return { target: { files: file ? [file] : [] } }; | ||
| } | ||
|
|
||
| describe('SystemService BIRT report design upload', () => { | ||
| let service: SystemService; | ||
| let httpMock: HttpTestingController; | ||
|
|
||
| beforeEach(() => { | ||
| TestBed.configureTestingModule({ | ||
| providers: [ | ||
| SystemService, | ||
| provideHttpClient(), | ||
| provideHttpClientTesting() | ||
| ] | ||
| }); | ||
|
|
||
| service = TestBed.inject(SystemService); | ||
| httpMock = TestBed.inject(HttpTestingController); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| httpMock.verify(); | ||
| }); | ||
|
|
||
| it('posts the design as multipart form data to the BIRT reports resource', () => { | ||
| const design = fileOf('Active Loans.rptdesign', 2048); | ||
|
|
||
| service.uploadBirtReportFile(design).subscribe(); | ||
|
|
||
| const req = httpMock.expectOne('/birt/reports'); | ||
| expect(req.request.method).toBe('POST'); | ||
| expect(req.request.body instanceof FormData).toBe(true); | ||
| expect((req.request.body as FormData).get('file')).toBe(design); | ||
| req.flush({ fileName: 'Active Loans.rptdesign', size: 2048, overwritten: false }); | ||
| }); | ||
|
|
||
| it('leaves the multipart Content-Type to the browser so the boundary is set', () => { | ||
| service.uploadBirtReportFile(fileOf('Active Loans.rptdesign', 10)).subscribe(); | ||
|
|
||
| const req = httpMock.expectOne('/birt/reports'); | ||
| expect(req.request.headers.get('Content-Type')).toBeNull(); | ||
| req.flush({}); | ||
| }); | ||
|
|
||
| it('surfaces a server rejection to the caller instead of swallowing it', () => { | ||
| let status: number | undefined; | ||
| service.uploadBirtReportFile(fileOf('Active Loans.rptdesign', 10)).subscribe({ | ||
| error: (error) => (status = error.status) | ||
| }); | ||
|
|
||
| httpMock.expectOne('/birt/reports').flush({}, { status: 403, statusText: 'Forbidden' }); | ||
| expect(status).toBe(403); | ||
| }); | ||
| }); | ||
|
|
||
| describe('UploadReportFileDialogComponent', () => { | ||
| let component: UploadReportFileDialogComponent; | ||
|
|
||
| beforeEach(() => { | ||
| component = new UploadReportFileDialogComponent(); | ||
| }); | ||
|
|
||
| it('accepts a .rptdesign file and reports its size', () => { | ||
| component.onFileSelect(changeEvent(fileOf('Active Loans.rptdesign', 2048))); | ||
|
|
||
| expect(component.file?.name).toBe('Active Loans.rptdesign'); | ||
| expect(component.errorKey).toBeNull(); | ||
| expect(component.fileSizeInKb).toBe('2.0'); | ||
| }); | ||
|
|
||
| it('accepts a .RPTDESIGN file whatever the case of the extension', () => { | ||
| component.onFileSelect(changeEvent(fileOf('Active Loans.RPTDESIGN', 10))); | ||
|
|
||
| expect(component.file).not.toBeNull(); | ||
| }); | ||
|
|
||
| it.each([ | ||
| 'report.pdf', | ||
| 'report.xml', | ||
| 'report.txt', | ||
| 'report.zip', | ||
| 'report.json', | ||
| 'report.prpt', | ||
| 'report' | ||
| ])('rejects %s', (name) => { | ||
| component.onFileSelect(changeEvent(fileOf(name, 10))); | ||
|
|
||
| expect(component.file).toBeNull(); | ||
| expect(component.errorKey).toBe('labels.text.Only Eclipse BIRT report designs can be uploaded'); | ||
| }); | ||
|
|
||
| it('rejects a design larger than the platform accepts', () => { | ||
| component.onFileSelect(changeEvent(fileOf('Active Loans.rptdesign', MAX_REPORT_DESIGN_SIZE_BYTES + 1))); | ||
|
|
||
| expect(component.file).toBeNull(); | ||
| expect(component.errorKey).toBe('labels.text.The selected report design is too large'); | ||
| }); | ||
|
|
||
| it('rejects an empty design', () => { | ||
| component.onFileSelect(changeEvent(fileOf('Active Loans.rptdesign', 0))); | ||
|
|
||
| expect(component.file).toBeNull(); | ||
| expect(component.errorKey).toBe('labels.text.The selected report design is empty'); | ||
| }); | ||
|
|
||
| it('clears a previous rejection when a valid design is chosen next', () => { | ||
| component.onFileSelect(changeEvent(fileOf('report.pdf', 10))); | ||
| component.onFileSelect(changeEvent(fileOf('Active Loans.rptdesign', 10))); | ||
|
|
||
| expect(component.errorKey).toBeNull(); | ||
| expect(component.file).not.toBeNull(); | ||
| }); | ||
|
|
||
| it('keeps nothing selected when the picker is dismissed', () => { | ||
| component.onFileSelect(changeEvent(fileOf('Active Loans.rptdesign', 10))); | ||
| component.onFileSelect(changeEvent(null)); | ||
|
|
||
| expect(component.file).toBeNull(); | ||
| expect(component.errorKey).toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| /** | ||
| * The upload button in Manage Reports is gated on CREATE_REPORT. This exercises that guard through | ||
| * the same directive the template uses, rather than rendering the whole reports screen. | ||
| */ | ||
| @Component({ | ||
| standalone: true, | ||
| imports: [HasPermissionDirective], | ||
| template: `<button *mifosxHasPermission="'CREATE_REPORT'" id="upload">Upload Report Design</button>` | ||
| }) | ||
| class UploadButtonHostComponent {} | ||
|
|
||
| describe('Upload Report Design button visibility', () => { | ||
| let rbacWasEnabled: boolean; | ||
|
|
||
| function renderWithPermissions(permissions: string[]): ComponentFixture<UploadButtonHostComponent> { | ||
| TestBed.configureTestingModule({ | ||
| imports: [UploadButtonHostComponent], | ||
| providers: [ | ||
| { provide: AuthenticationService, useValue: { getCredentials: () => ({ permissions }) } } | ||
| ] | ||
| }); | ||
| const fixture = TestBed.createComponent(UploadButtonHostComponent); | ||
| fixture.detectChanges(); | ||
| return fixture; | ||
| } | ||
|
|
||
| function uploadButton(fixture: ComponentFixture<UploadButtonHostComponent>): HTMLElement | null { | ||
| return fixture.nativeElement.querySelector('#upload'); | ||
| } | ||
|
|
||
| beforeEach(() => { | ||
| rbacWasEnabled = environment.productionModeEnableRBAC; | ||
| environment.productionModeEnableRBAC = true; | ||
| TestBed.resetTestingModule(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| environment.productionModeEnableRBAC = rbacWasEnabled; | ||
| }); | ||
|
|
||
| it('is shown to a user with CREATE_REPORT', () => { | ||
| expect(uploadButton(renderWithPermissions(['CREATE_REPORT']))).not.toBeNull(); | ||
| }); | ||
|
|
||
| it('is shown to a super user', () => { | ||
| expect(uploadButton(renderWithPermissions(['ALL_FUNCTIONS']))).not.toBeNull(); | ||
| }); | ||
|
|
||
| it('is hidden from a user who may only read reports', () => { | ||
| expect(uploadButton(renderWithPermissions(['READ_REPORT']))).toBeNull(); | ||
| }); | ||
|
|
||
| it('is hidden from a user with a blanket read grant', () => { | ||
| expect(uploadButton(renderWithPermissions(['ALL_FUNCTIONS_READ']))).toBeNull(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
.../system/manage-reports/upload-report-file-dialog/upload-report-file-dialog.component.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| <!-- | ||
| Copyright since 2025 Mifos Initiative | ||
|
|
||
| This Source Code Form is subject to the terms of the Mozilla Public | ||
| License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| --> | ||
|
|
||
| <h1 mat-dialog-title>{{ 'labels.heading.Upload Report Design' | translate }}</h1> | ||
|
|
||
| <div class="m-l-10 m-r-10"> | ||
| <p class="report-design-hint">{{ 'labels.text.Upload a BIRT report design' | translate }}</p> | ||
|
|
||
| <mifosx-file-upload flex="60%" [acceptFilter]="acceptFilter" (change)="onFileSelect($event)"></mifosx-file-upload> | ||
|
|
||
| @if (file) { | ||
| <p class="report-design-summary"> | ||
| <span class="report-design-name">{{ file.name }}</span> | ||
| <span class="report-design-size">{{ fileSizeInKb }} {{ 'labels.text.KB' | translate }}</span> | ||
| </p> | ||
| } | ||
|
|
||
| @if (errorKey) { | ||
| <p class="report-design-error">{{ errorKey | translate }}</p> | ||
| } | ||
|
|
||
| <mat-dialog-actions align="end"> | ||
| <button mat-raised-button mat-dialog-close>{{ 'labels.buttons.Cancel' | translate }}</button> | ||
| <button mat-raised-button color="primary" [disabled]="!file" [mat-dialog-close]="{ file: file }"> | ||
| {{ 'labels.buttons.Upload' | translate }} | ||
| </button> | ||
| </mat-dialog-actions> | ||
| </div> |
34 changes: 34 additions & 0 deletions
34
.../system/manage-reports/upload-report-file-dialog/upload-report-file-dialog.component.scss
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| /** | ||
| * Copyright since 2025 Mifos Initiative | ||
| * | ||
| * This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| */ | ||
|
|
||
| .report-design-hint { | ||
| color: var(--mat-sys-on-surface-variant); | ||
| font-size: 0.85rem; | ||
| } | ||
|
|
||
| .report-design-summary { | ||
| display: flex; | ||
| justify-content: space-between; | ||
| gap: 1rem; | ||
| font-size: 0.85rem; | ||
| } | ||
|
|
||
| .report-design-name { | ||
| overflow-wrap: anywhere; | ||
| font-weight: 500; | ||
| } | ||
|
|
||
| .report-design-size { | ||
| white-space: nowrap; | ||
| color: var(--mat-sys-on-surface-variant); | ||
| } | ||
|
|
||
| .report-design-error { | ||
| color: var(--mat-sys-error); | ||
| font-size: 0.85rem; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.