Skip to content

Commit

Permalink
Merge pull request #252 from e-picsa/ft-enable-xls-parsing
Browse files Browse the repository at this point in the history
Ft enable xls parsing
  • Loading branch information
chrismclarke authored Mar 28, 2024
2 parents be610ec + ce10297 commit 4793aa3
Show file tree
Hide file tree
Showing 10 changed files with 290 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { RouterModule } from '@angular/router';

import { FormSubmissionsComponent } from './pages/form-submissions/form-submissions.component';
import { MonitoringPageComponent } from './pages/home/monitoring.page';
import { UpdateMonitoringFormsComponent } from './pages/update/update-monitoring-forms.component';
import { ViewMonitoringFormsComponent } from './pages/view/view-monitoring-forms.component';

@NgModule({
Expand All @@ -23,6 +24,10 @@ import { ViewMonitoringFormsComponent } from './pages/view/view-monitoring-forms
path: ':id/submissions',
component: FormSubmissionsComponent,
},
{
path: ':id/edit',
component: UpdateMonitoringFormsComponent,
},
]),
],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
// eslint-disable-next-line @nx/enforce-module-boundaries
import { Database } from '@picsa/server-types';
import { PicsaAsyncService } from '@picsa/shared/services/asyncService.service';
import { PicsaNotificationService } from '@picsa/shared/services/core/notification.service';
import { SupabaseService } from '@picsa/shared/services/core/supabase';
import { IStorageEntry } from '@picsa/shared/services/core/supabase/services/supabase-storage.service';
import { firstValueFrom, Observable } from 'rxjs';

export type IMonitoringFormsRow = Database['public']['Tables']['monitoring_forms']['Row'];

Expand All @@ -21,7 +24,11 @@ export class MonitoringFormsDashboardService extends PicsaAsyncService {
return this.supabaseService.db.table(this.TABLE_NAME);
}

constructor(private supabaseService: SupabaseService) {
constructor(
private supabaseService: SupabaseService,
private http: HttpClient,
private notificationService: PicsaNotificationService
) {
super();
}

Expand Down Expand Up @@ -57,4 +64,74 @@ export class MonitoringFormsDashboardService extends PicsaAsyncService {
}
return { data, error };
}

public async updateFormById(id: string, updatedForm: Partial<IMonitoringFormsRow>): Promise<IMonitoringFormsRow> {
const { data, error } = await this.supabaseService.db
.table(this.TABLE_NAME)
.update(updatedForm)
.eq('id', id)
.select()
.single();
if (error) {
throw error;
}
return data;
}

/**
* Convert an xls form to xml-xform standard
* @param file xls file representation
* @returns xml string of converted form
*/
async submitFormToConvertXlsToXForm(file: File) {
const url = 'https://xform-converter.picsa.app/api/v1/convert';
try {
const { result } = await firstValueFrom(this.http.post(url, file) as Observable<XFormConvertRes>);
return result;
} catch (error: any) {
console.error(error);
this.notificationService.showUserNotification({ matIcon: 'error', message: error?.message || error });
return null;
}
}
/**
* Convert
* @param formData formData object with 'files' property that includes xml xform read as a File
* @returns enketo entry of converted xmlform
*/
async submitFormToConvertXFormToEnketo(formData: FormData) {
const url = 'https://enketo-converter.picsa.app/api/xlsform-to-enketo';
try {
const { convertedFiles } = await firstValueFrom(this.http.post(url, formData) as Observable<IEnketoConvertRes>);
return convertedFiles[0]?.content;
} catch (error: any) {
console.error(error);
this.notificationService.showUserNotification({ matIcon: 'error', message: error?.message || error });
return null;
}
}
}
/** Response model returned from xform-converter */
interface XFormConvertRes {
/** http error if thrown */
error: any;
/** xml string of converted */
result: string;
/** https status code, 200 indicates success */
status: number;
}
/** Response model returned from enketo-converter */
interface IEnketoConvertRes {
convertedFiles: {
content: IEnketoConvertContent;
filename: string;
}[];
message: string;
}
interface IEnketoConvertContent {
form: string;
languageMap: any;
model: string;
theme: string;
transformerVersion: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<div class="page-content">
<div style="display: flex; align-items: center">
<h2 style="flex: 1">Update Form</h2>
</div>
@if(form){
<form class="form-content">
<h2 style="flex: 1">Upload new Form excel file</h2>
<picsa-supabase-upload
[fileTypes]="allowedFileTypes"
[autoUpload]="false"
[storageBucketName]="storageBucketName"
[storageFolderPath]="storageFolderPath"
(uploadComplete)="handleUploadComplete($event)"
>
</picsa-supabase-upload>
</form>
} @if(updateFeedbackMessage) {
<div>{{ updateFeedbackMessage }}</div>
} @if(uploading==true) {
<div>Uploading form...</div>
}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.form-content {
display: flex;
flex-direction: column;
gap: 1.4rem;
}
.submitButton {
width: 7rem;
margin-bottom: 1rem;
}
.form-data {
display: flex;
flex-direction: column;
gap: 0.5rem;
}

.data-container {
margin-left: 2rem;
max-height: 25rem;
overflow-y: auto;
}
label {
font-weight: 700;
}
.action-button-section {
display: flex;
flex-direction: row;
gap: 5px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { UpdateMonitoringFormsComponent } from './update-monitoring-forms.component';

describe('UpdateMonitoringFormsComponent', () => {
let component: UpdateMonitoringFormsComponent;
let fixture: ComponentFixture<UpdateMonitoringFormsComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UpdateMonitoringFormsComponent],
}).compileComponents();

fixture = TestBed.createComponent(UpdateMonitoringFormsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, RouterModule } from '@angular/router';
// eslint-disable-next-line @nx/enforce-module-boundaries
import type { Database } from '@picsa/server-types';
import {
IUploadResult,
SupabaseStoragePickerDirective,
SupabaseUploadComponent,
} from '@picsa/shared/services/core/supabase';
import { SupabaseStorageService } from '@picsa/shared/services/core/supabase/services/supabase-storage.service';
import { NgxJsonViewerModule } from 'ngx-json-viewer';

import { DashboardMaterialModule } from '../../../../material.module';
import { MonitoringFormsDashboardService } from '../../monitoring.service';

export type IMonitoringFormsRow = Database['public']['Tables']['monitoring_forms']['Row'];

@Component({
selector: 'dashboard-monitoring-update',
standalone: true,
imports: [
CommonModule,
DashboardMaterialModule,
FormsModule,
ReactiveFormsModule,
RouterModule,
NgxJsonViewerModule,
SupabaseUploadComponent,
SupabaseStoragePickerDirective,
],
templateUrl: './update-monitoring-forms.component.html',
styleUrls: ['./update-monitoring-forms.component.scss'],
})
export class UpdateMonitoringFormsComponent implements OnInit {
public form: IMonitoringFormsRow;
public updateFeedbackMessage = '';
public uploading = false;
public allowedFileTypes = ['xlsx', 'xls'].map((ext) => `.${ext}`);
public storageBucketName = 'global';
public storageFolderPath = 'monitoring/forms';
constructor(
private service: MonitoringFormsDashboardService,
private route: ActivatedRoute,
private storageService: SupabaseStorageService
) {}
async ngOnInit() {
await this.service.ready();
this.route.params.subscribe(async (params) => {
const id = params['id'];
this.service
.getFormById(id)
.then((data) => {
this.form = data;
})
.catch((error) => {
console.error('Error fetching Form:', error);
});
});
}

public async handleUploadComplete(res: IUploadResult[]) {
if (res.length === 0) {
return;
}
// As conversion is a 2-step process (xls file -> xml form -> enketo form) track progress
// so that uploaded file can be removed if not successful
let xformConversionSuccess = false;
this.uploading = true;
const [{ data, entry }] = res;

const xform = await this.service.submitFormToConvertXlsToXForm(data as File);

if (xform) {
const blob = new Blob([xform], { type: 'text/xml' });
const xmlFile = new File([blob], 'form.xml', { type: 'text/xml' });
const formData = new FormData();
formData.append('files', xmlFile);

const enketoContent = await this.service.submitFormToConvertXFormToEnketo(formData);
if (enketoContent) {
const { form, languageMap, model, theme } = enketoContent;
// Update db entry with form_xlsx
this.form = await this.service.updateFormById(this.form.id, {
form_xlsx: `${this.storageBucketName}/${this.storageFolderPath}/${entry.name}`,
enketo_form: form,
enketo_model: model,
enketo_definition: { ...(this.form.enketo_definition as any), languageMap, theme },
});
this.updateFeedbackMessage = 'Form updated successfully!';
this.uploading = false;
xformConversionSuccess = true;
}
}
// If conversion not successful delete file from storage
if (!xformConversionSuccess) {
const storagePath = `${this.storageFolderPath}/${entry.name}`;
const { error } = await this.storageService.deleteFile(this.storageBucketName, storagePath);
if (error) throw error;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
<div style="display: flex; align-items: center">
<h2 style="flex: 1">Monitoring Form View</h2>
@if(form){
<button mat-raised-button color="primary" routerLink="submissions">View Submissions</button>
<div class="action-button-section">
<button mat-raised-button color="primary" routerLink="edit">Edit Form</button>
<button mat-raised-button color="primary" routerLink="submissions">View Submissions</button>
</div>
}
</div>
@if(form){
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
.form-content{
display: flex;
flex-direction: column;
gap: 1.4rem;
.form-content {
display: flex;
flex-direction: column;
gap: 1.4rem;
}
.submitButton{
width: 7rem;
margin-bottom: 1rem;
.submitButton {
width: 7rem;
margin-bottom: 1rem;
}
.form-data{
display: flex;
flex-direction: column;
gap: 0.5rem;
.form-data {
display: flex;
flex-direction: column;
gap: 0.5rem;
}

.data-container{
margin-left: 2rem;
max-height: 25rem;
overflow-y: auto;
.data-container {
margin-left: 2rem;
max-height: 25rem;
overflow-y: auto;
}
label {
font-weight: 700;
}
.action-button-section {
display: flex;
flex-direction: row;
gap: 5px;
}
label{
font-weight: 700;

}
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export class SupabaseUploadComponent {

private async checkDuplicateUpload(file: UppyFile) {
const storageFile = await this.storageService.getFile({
bucketId: 'resources',
bucketId: this.storageBucketName,
filename: file.name,
folderPath: this.storageFolderPath || '',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ export class SupabaseStorageService {
return data?.[0] || null;
}

public async deleteFile(bucketId: string, filePath: string) {
return this.storage.from(bucketId).remove([filePath]);
}

/** Return the link to a file in a public bucket */
public getPublicLink(bucketId: string, objectPath: string) {
return this.storage.from(bucketId).getPublicUrl(objectPath).data.publicUrl;
Expand Down

0 comments on commit 4793aa3

Please sign in to comment.