Skip to content
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

Ft enable xls parsing #252

Merged
merged 6 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading