Skip to content

Commit add4e94

Browse files
feat(mapper): updates to web forms submission image upload (#2292)
* hide conflicting footer when web forms drawer is open * fix: Submission details page crashing when fetching photos fails * feature: improved web forms submission * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fixed stepper layout --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 491b27e commit add4e94

File tree

3 files changed

+96
-10
lines changed

3 files changed

+96
-10
lines changed

src/mapper/src/lib/components/forms/wrapper.svelte

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,96 @@
22
import type { SlDrawer } from '@shoelace-style/shoelace';
33
44
import { getCommonStore } from '$store/common.svelte.ts';
5+
import { getLoginStore } from '$store/login.svelte.ts';
6+
7+
const CUSTOM_UPLOAD_ELEMENT_ID = '95c6807c-82b4-4208-b5df-5a3e795227b0';
58
69
const API_URL = import.meta.env.VITE_API_URL;
710
type Props = {
811
display: Boolean;
912
entityId: string | undefined;
1013
projectId: number | undefined;
14+
taskId: number | undefined;
1115
webFormsRef: HTMLElement | undefined;
1216
};
1317
const commonStore = getCommonStore();
14-
let { display = $bindable(false), entityId, webFormsRef = $bindable(undefined), projectId }: Props = $props();
18+
const loginStore = getLoginStore();
19+
20+
let { display = $bindable(false), entityId, webFormsRef = $bindable(undefined), projectId, taskId }: Props = $props();
1521
let drawerRef: SlDrawer;
16-
let iframeRef: HTMLIFrameElement;
22+
let iframeRef: HTMLIFrameElement | undefined;
1723
let odkForm: any;
18-
// to-do: hide drawer upon successful submission
24+
let startDate: string | undefined;
25+
26+
let pic: any;
27+
28+
function handleMutation() {
29+
if (!iframeRef) return;
30+
if (!iframeRef.contentDocument) return;
31+
if (!odkForm) return;
32+
if (!webFormsRef) return;
33+
34+
// check if we've already added the custom upload input
35+
if (iframeRef.contentDocument.getElementById(CUSTOM_UPLOAD_ELEMENT_ID)) return;
36+
console.log('over-writing control node');
37+
38+
const uploadControl = odkForm.getChildren().find((n: any) => n.constructor.name === 'UploadControl');
39+
const uploadControlNodeId = uploadControl.nodeId;
40+
const uploadControlNodeSelector = `[id='${uploadControlNodeId}_container']`;
41+
const uploadControlNode = webFormsRef.querySelector(uploadControlNodeSelector);
42+
if (!uploadControlNode) return;
43+
44+
const controlText = iframeRef.contentDocument.createElement('div');
45+
controlText.id = CUSTOM_UPLOAD_ELEMENT_ID;
46+
controlText.className = 'control-text';
47+
controlText.style.marginBottom = '.75rem';
48+
49+
const label = iframeRef.contentDocument.createElement('label');
50+
label.style.fontWeight = '400';
51+
label.style.lineHeight = '1.8rem';
52+
const span = iframeRef.contentDocument.createElement('span');
53+
span.textContent = uploadControl.engineState.label.chunks.map((chunk: any) => chunk.asString).join(' ');
54+
label.appendChild(span);
55+
controlText.appendChild(label);
56+
const inputWrapper = iframeRef.contentDocument.createElement('div');
57+
const input = iframeRef.contentDocument.createElement('input');
58+
input.id = CUSTOM_UPLOAD_ELEMENT_ID;
59+
input.addEventListener('change', () => {
60+
pic = input.files?.[0];
61+
});
62+
input.type = 'file';
63+
inputWrapper.appendChild(input);
64+
65+
uploadControlNode.innerHTML = '';
66+
uploadControlNode.appendChild(controlText);
67+
uploadControlNode.appendChild(inputWrapper);
68+
}
1969
function handleSubmit(payload: any) {
2070
(async () => {
2171
if (!payload.detail) return;
72+
73+
let submission_xml = await payload.detail[0].data.instanceFile.text();
74+
75+
submission_xml = submission_xml.replace('<start/>', `<start>${startDate}</start>`);
76+
submission_xml = submission_xml.replace('<end/>', `<end>${new Date().toISOString()}</end>`);
77+
78+
const authDetails = loginStore?.getAuthDetails;
79+
if (authDetails?.username) {
80+
submission_xml = submission_xml.replace('<username/>', `<username>${authDetails?.username}</username>`);
81+
}
82+
83+
if (authDetails?.email_address) {
84+
submission_xml = submission_xml.replace('<email/>', `<email>${authDetails?.email_address}</email>`);
85+
}
86+
87+
if (pic && pic?.name) {
88+
submission_xml = submission_xml.replace('<image/>', `<image>${pic?.name}</image>`);
89+
}
90+
2291
const url = `${API_URL}/submission?project_id=${projectId}`;
2392
var data = new FormData();
24-
data.append('submission_xml', await payload.detail[0].data.instanceFile.text());
93+
data.append('submission_xml', submission_xml);
94+
data.append('submission_files', pic);
2595
await fetch(url, {
2696
method: 'POST',
2797
body: data,
@@ -35,11 +105,12 @@
35105
console.log('set odkForm', odkForm);
36106
// over-write default language
37107
setFormLanguage(commonStore.locale);
108+
109+
const nodes = odkForm.getChildren();
110+
38111
// select feature that you clicked on map
39-
odkForm
40-
.getChildren()
41-
.find((it: any) => it.definition.nodeset === '/data/feature')
42-
?.setValueState([entityId]);
112+
nodes.find((it: any) => it.definition.nodeset === '/data/feature')?.setValueState([entityId]);
113+
nodes.find((it: any) => it.definition.nodeset === '/data/task_id')?.setValueState(`${taskId}`);
43114
}
44115
}
45116
const setFormLanguage = (newLocale: string) => {
@@ -51,6 +122,15 @@
51122
}
52123
}
53124
};
125+
$effect(() => {
126+
// making sure we re-run whenever one of the following reactive variables changes
127+
display;
128+
projectId;
129+
entityId;
130+
if (display) {
131+
startDate = new Date().toISOString();
132+
}
133+
});
54134
$effect(() => {
55135
if (commonStore.locale) {
56136
setFormLanguage(commonStore.locale);
@@ -63,17 +143,21 @@
63143
projectId;
64144
entityId;
65145
if (iframeRef) {
146+
const observer = new MutationObserver(handleMutation);
66147
const intervalId = setInterval(() => {
67148
webFormsRef = iframeRef?.contentDocument?.querySelector('odk-web-form') || undefined;
68149
if (webFormsRef) {
69150
clearInterval(intervalId);
70151
webFormsRef.addEventListener('submit', handleSubmit);
71152
webFormsRef.addEventListener('odkForm', handleOdkForm);
153+
observer.observe(webFormsRef, { attributes: true, childList: true, subtree: true });
72154
}
73155
}, 10);
156+
74157
// clear interval when re-run
75158
return () => {
76159
clearInterval(intervalId);
160+
observer.disconnect();
77161
};
78162
}
79163
});
@@ -104,7 +188,7 @@
104188
bind:this={iframeRef}
105189
title="odk-web-forms-wrapper"
106190
src={`./web-forms.html?projectId=${projectId}&entityId=${entityId}&api_url=${API_URL}&language=${commonStore.locale}`}
107-
style="height: {window.outerHeight}px; width: 100%; z-index: 11;"
191+
style="height: 100%; width: 100%; z-index: 11;"
108192
>
109193
</iframe>
110194
{/key}

src/mapper/src/routes/[projectId]/+page.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,8 @@
381381
bind:webFormsRef
382382
bind:display={displayWebFormsDrawer}
383383
projectId={data?.projectId}
384-
entityId={selectedEntityId}
384+
entityId={selectedEntityId || undefined}
385+
taskId={taskStore.selectedTaskId || undefined}
385386
/>
386387
</div>
387388

src/mapper/src/store/login.svelte.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
type authDetailsType = {
22
id: number;
33
username: string;
4+
email_address?: string;
45
picture: string;
56
role: string;
67
// Here we omit project_roles and orgs_managed as they are not needed

0 commit comments

Comments
 (0)