Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
0df57c6
feat(#10904): route file attachments to correct sub-docs in report fo…
YASHSHARMAOFFICIALLY May 19, 2026
0cf0555
feat(#10904): route file attachments to correct sub-docs in report forms
YASHSHARMAOFFICIALLY Apr 24, 2026
1da592d
feat(#10903): route attachments per-doc in ContactSaveService
benkags Apr 22, 2026
f909b74
feat(#10903): validate attachment size on contact save path
benkags Apr 22, 2026
c423416
test(#10903): cover sub-contact attachment routing
benkags Apr 22, 2026
d47e791
test(#10903): cover validateAttachments wiring on contact save path
benkags Apr 22, 2026
de4b26e
fix(#10903): match type="file" upload nodes when routing
benkags Apr 23, 2026
343bf01
feat(#10903): clean up.
benkags Apr 24, 2026
5885ee1
refactor `resolveContactOwnerDoc` signature.
benkags Apr 27, 2026
ba8cc48
refactor `resolveContactOwnerDoc` to reduce cognitive load
benkags Apr 27, 2026
7d3be60
refactor: use Set for ignored section tags
benkags Apr 28, 2026
85f42d6
refactor: use Array.find over jQuery filter()[0]
benkags Apr 28, 2026
592542e
refactor: group contact-owner lookup params into context object
benkags Apr 28, 2026
feb2c3b
test(e2e): assert sub-contact attachment routing on create and edit
benkags May 6, 2026
f37a309
test(e2e): click add-repeat for the first child row, fix edit-flow re…
benkags May 6, 2026
76ad82b
test(e2e): drop primary contact section from edit form
benkags May 6, 2026
8c50eae
test(e2e): rename main section to match contact_type so existing data…
benkags May 6, 2026
a1d5aaa
Apply suggestions from code review
benkags May 8, 2026
9bf863b
feat(#10904): unify inline-binary field handling with file-widget att…
benkags May 14, 2026
517ffc0
fix typo.
benkags May 16, 2026
146de34
fix(#10903): route FileManager uploads by [type=file] only
benkags May 16, 2026
12f96a1
feat(#10904): unify media field value with attachment name
benkags May 20, 2026
59477f0
test(e2e): assert badge value equals attachment name minus prefix
benkags May 20, 2026
c6d62cd
fix(#10904): namespace report sub-doc binaries by ownerDoc.form
benkags May 20, 2026
cfb16eb
fix(#10904): resolve rebase conflict in enketo report-routing specs
benkags May 21, 2026
78b6494
Bump CI
jkuester May 21, 2026
dd8a4d5
comments clean up.
benkags May 21, 2026
844dcdf
fix failing e2e test.
benkags May 21, 2026
1fee702
fix failing e2e test.
benkags May 21, 2026
8854033
test(#10903): add tests and clean up
benkags May 21, 2026
77fbff1
fix(#10904): preserve untouched inline-binary fields on edit via sidecar
benkags Jun 3, 2026
47dafc1
refactor: reduce recoverEmptyBinaryReference cognitive complexity fro…
benkags Jun 3, 2026
1a80d1a
refactor(#10904): drop recoverEmptyBinaryReference, pin sidecar contract
benkags Jun 9, 2026
86e527e
fix(#10904): preserve untouched inline-binary contact fields on edit …
benkags Jun 10, 2026
82b3b75
fix(#10904): route repeat-child attachments by <child> index
benkags Jun 20, 2026
e5bab65
refactor: unify report & contact attachment-routing pipelines
benkags Jun 21, 2026
33ac515
refactor(#10904): name inline-binary attachments by owner-relative xpath
benkags Jun 22, 2026
451c9fd
test(#10904): cover sidecar restore for sibling and repeat-child bina…
benkags Jul 2, 2026
8883448
Merge branch 'master' into 10903-sub-contact-routing
benkags Jul 6, 2026
1158059
fix(#10904): route draw/signature uploads to their owning sub-doc
benkags Jul 6, 2026
d50f56c
fix(#10904): consume matched upload nodes to route same-named files
benkags Jul 6, 2026
d18a260
test(#10904): cover nested sub-doc image render and sub-doc orphan cl…
benkags Jul 6, 2026
66f65a8
refactor(#10904): move attachment-routing primitives to a provider
benkags Jul 6, 2026
2f6c1be
test(#10904): use specific chai assertions flagged by SonarCloud
benkags Jul 6, 2026
0713dd8
fix(#10904): repair provider spec fixture path after the move
benkags Jul 6, 2026
6e2c055
Merge branch 'master' into fork/benkags/10903-sub-contact-routing
jkuester Jul 31, 2026
2c7757f
Initial refactoring
jkuester Aug 3, 2026
7a78a00
Clean up binary attachment naming to only include the path
jkuester Aug 3, 2026
f26cfd8
Fix binary attachment handling for edit contact forms
jkuester Aug 3, 2026
d66c790
Fix contact edit file attachment handling
jkuester Aug 4, 2026
7990ce1
Clean up unit tests
jkuester Aug 4, 2026
7bae381
Fix prepopulation-data service issue
jkuester Aug 4, 2026
8f3e65e
Merge branch 'master' into 10903-sub-contact-routing-merge
jkuester Aug 4, 2026
98a7584
Clean up basic contact-attachments form to properly include binary at…
jkuester Aug 4, 2026
ff1916d
Clean up sub-contact-attachments e2e test
jkuester Aug 5, 2026
3364974
Update binary attachment naming to include report `fields` in name
jkuester Aug 5, 2026
bb9e82a
Rename photo-upload test
jkuester Aug 5, 2026
ae5c81a
Clean up e2e tests
jkuester Aug 5, 2026
46ebcbc
Update report db-doc attachment tests to one-shot the report
jkuester Aug 5, 2026
8617c7f
Fix bugs in format-data-record service around xpaths
jkuester Aug 6, 2026
9d7d68c
Fix pregnancy test broken by fixing hidden_fields in repeats
jkuester Aug 6, 2026
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ config/*/forms/contact/*.xlsx
/**/*/.snapshots/local.json
tests/integration/results/
allure*
tests/config-temp
tests/utils/config-temp
.eslintcache
doc-conflicts/
Expand All @@ -61,4 +62,4 @@ user-password-change.csv
/tests/e2e/visual/images/*.png
.envrc
release-notes.md
/scripts/build/helm/values.yaml
/scripts/build/helm/values.yaml
1 change: 0 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ module.exports = defineConfig([
'shared-libs/cht-datasource/docs/**/*',
'tests/scalability/report*/**/*',
'tests/scalability/jmeter/**/*',
'webapp/src/ts/providers/xpath-element-path.provider.ts',
'webapp/dist/**/*',
'.github/**/compiled/index.js'
]),
Expand Down
85 changes: 16 additions & 69 deletions tests/e2e/default/contacts/contact-attachments.wdio-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const { CONTACT_TYPES } = require('@medic/constants');
describe('Contact form attachments', () => {
const photoPngPath = path.join(__dirname, '../enketo/images/photo-for-upload-form.png');
const layersPngPath = path.join(__dirname, '../../../../webapp/src/img/layers.png');

const places = placeFactory.generateHierarchy();
const healthCenter = places.get(CONTACT_TYPES.HEALTH_CENTER);

Expand Down Expand Up @@ -44,42 +43,15 @@ describe('Contact form attachments', () => {
'contact.type.person_with_attachments.edit': 'Edit Person With Attachments'
};

const createFormXml = fs.readFileSync(
path.join(__dirname, 'forms/person-with-attachments-create.xml'),
'utf8'
const createFormDoc = commonPage.createFormDoc(
path.join(__dirname, 'forms/person-with-attachments-create'),
'contact:person_with_attachments:create'
);

const editFormXml = fs.readFileSync(
path.join(__dirname, 'forms/person-with-attachments-edit.xml'),
'utf8'
const editFormDoc = commonPage.createFormDoc(
path.join(__dirname, 'forms/person-with-attachments-edit'),
'contact:person_with_attachments:edit'
);

const createFormDoc = {
_id: 'form:contact:person_with_attachments:create',
internalId: 'contact:person_with_attachments:create',
title: 'New Person With Attachments',
type: 'form',
_attachments: {
xml: {
content_type: 'application/octet-stream',
data: Buffer.from(createFormXml).toString('base64'),
}
}
};

const editFormDoc = {
_id: 'form:contact:person_with_attachments:edit',
internalId: 'contact:person_with_attachments:edit',
title: 'Edit Person With Attachments',
type: 'form',
_attachments: {
xml: {
content_type: 'application/octet-stream',
data: Buffer.from(editFormXml).toString('base64'),
}
}
};

const createContactWithAttachment = (contactName, imagePath = photoPngPath) => {
const imageBuffer = fs.readFileSync(imagePath);
const imageBase64 = imageBuffer.toString('base64');
Expand Down Expand Up @@ -128,37 +100,6 @@ describe('Contact form attachments', () => {
await commonPage.waitForPageLoaded();
});

it('should create contact with image attachment', async () => {
const contactName = 'Test Person With Photo';

await commonPage.goToPeople(healthCenter._id);
await commonPage.clickFastActionFAB({ actionId: personWithAttachmentsType.id });

await commonEnketoPage.setInputValue('Full name', contactName);
await commonEnketoPage.addFileInputValue('Photo', photoPngPath);

await genericForm.submitForm();
await commonPage.waitForPageLoaded();
await contactPage.waitForContactLoaded();

const contactId = await contactPage.getCurrentContactId();
expect(contactId).to.exist;

const createdContact = await utils.getDoc(contactId);

expect(createdContact).to.exist;
expect(createdContact.name).to.equal(contactName);
expect(createdContact._attachments).to.exist;

const attachmentNames = Object.keys(createdContact._attachments);
expect(attachmentNames).to.have.lengthOf(1);
expect(attachmentNames[0]).to.match(/^user-file-photo-for-upload-form.*\.png$/);

const attachment = createdContact._attachments[attachmentNames[0]];
expect(attachment.content_type).to.equal('image/png');
expect(attachment.length, 'Attachment should have a valid size').to.be.greaterThan(0);
});

it('should create contact with multiple attachments (image + document)', async () => {
const contactName = 'Test Person With Multiple Files';

Expand All @@ -183,7 +124,7 @@ describe('Contact form attachments', () => {
expect(createdContact._attachments).to.exist;

const attachmentNames = Object.keys(createdContact._attachments);
expect(attachmentNames).to.have.lengthOf(2);
expect(attachmentNames).to.have.lengthOf(3);

const photoAttachment = attachmentNames.find(name => name.match(/^user-file-photo-for-upload-form.*\.png$/));
const documentAttachment = attachmentNames.find(name => name.match(/^user-file-layers.*\.png$/));
Expand All @@ -198,6 +139,9 @@ describe('Contact form attachments', () => {
expect(createdContact._attachments[documentAttachment].content_type).to.equal('image/png');
expect(createdContact._attachments[documentAttachment].length, 'Document should have a valid size')
.to.be.greaterThan(0);

expect(createdContact._attachments['user-file/badge'].content_type).to.equal('image/png');
expect(createdContact._attachments['user-file/badge'].length).to.be.greaterThan(0);
});

it('should preserve attachments when editing contact', async () => {
Expand All @@ -221,7 +165,9 @@ describe('Contact form attachments', () => {
const contactBefore = await utils.getDoc(contactId);
expect(contactBefore._attachments).to.exist;
const originalAttachments = Object.keys(contactBefore._attachments);
expect(originalAttachments).to.have.lengthOf(1);
expect(originalAttachments).to.have.lengthOf(2);
expect(originalAttachments[0]).to.match(/^user-file-photo-for-upload-form.*\.png$/);
expect(originalAttachments[1]).to.equal('user-file/badge');

await commonPage.accessEditOption();

Expand All @@ -237,10 +183,11 @@ describe('Contact form attachments', () => {
expect(contactAfter._attachments).to.exist;

const attachmentsAfter = Object.keys(contactAfter._attachments);
expect(attachmentsAfter).to.have.lengthOf(1);
expect(attachmentsAfter[0]).to.equal(originalAttachments[0]);
expect(attachmentsAfter).to.deep.equal(originalAttachments);
expect(contactAfter._attachments[attachmentsAfter[0]].length, 'Preserved attachment should have a valid size')
.to.be.greaterThan(0);
expect(contactAfter._attachments[attachmentsAfter[1]].length, 'Preserved attachment should have a valid size')
.to.be.greaterThan(0);
});

it('should remove attachment when editing contact', async () => {
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?xml version="1.0"?>
<h:html xmlns="http://www.w3.org/2002/xforms" xmlns:h="http://www.w3.org/1999/xhtml" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:jr="http://openrosa.org/javarosa" xmlns:orx="http://openrosa.org/xforms" xmlns:odk="http://www.opendatakit.org/xforms" xmlns:cht="https://communityhealthtoolkit.org">
<h:head>
<h:title>New Health Center</h:title>
<model odk:xforms-version="1.0.0">
<itext>
<translation lang="en">
<text id="/data/parent:label">
<value>Parent District Hospital</value>
</text>
<text id="/data/parent/name:label">
<value>Parent Name</value>
</text>
<text id="/data/parent/badge:label">
<value>Parent Badge</value>
</text>
<text id="/data/parent/photo:label">
<value>Parent Photo</value>
</text>
<text id="/data/contact:label">
<value>Primary Contact for Heath Center</value>
</text>
<text id="/data/contact/name:label">
<value>Contact Name</value>
</text>
<text id="/data/contact/badge:label">
<value>Contact Badge</value>
</text>
<text id="/data/contact/photo:label">
<value>Contact Photo</value>
</text>
<text id="/data/health_center_with_attachments:label">
<value>Heath Center</value>
</text>
<text id="/data/health_center_with_attachments/name:label">
<value>Health Center Name</value>
</text>
<text id="/data/health_center_with_attachments/badge:label">
<value>Health Center Badge</value>
</text>
<text id="/data/health_center_with_attachments/photo:label">
<value>Health Center Photo</value>
</text>
<text id="/data/repeat:label">
<value>Add Clinics</value>
</text>
<text id="/data/repeat/child:label">
<value>Clinic</value>
</text>
<text id="/data/repeat/child/name:label">
<value>Child Name</value>
</text>
<text id="/data/repeat/child/badge:label">
<value>Child Badge</value>
</text>
<text id="/data/repeat/child/photo:label">
<value>Child Photo</value>
</text>
</translation>
</itext>
<instance>
<data id="contact:health_center-with-attachments:create" version="2026-08-05 11-44" prefix="J1!contact:health_center-with-attachments:create!" delimiter="#">
<parent>
<type/>
<name/>
<badge type="binary"/>
<photo/>
</parent>
<contact>
<parent>PARENT</parent>
<type/>
<name/>
<badge type="binary"/>
<photo/>
</contact>
<health_center_with_attachments>
<parent/>
<type/>
<contact/>
<name/>
<badge type="binary"/>
<photo/>
</health_center_with_attachments>
<repeat>
<child jr:template="">
<type/>
<name/>
<badge type="binary"/>
<photo/>
</child>
</repeat>
<meta tag="hidden">
<instanceID/>
</meta>
</data>
</instance>
<bind nodeset="/data/parent/type" type="string" calculate="&quot;district_hospital&quot;"/>
<bind nodeset="/data/parent/name" type="string" required="true()"/>
<bind nodeset="/data/parent/badge" type="string"/>
<bind nodeset="/data/parent/photo" type="binary"/>
<bind nodeset="/data/contact/parent" type="string"/>
<bind nodeset="/data/contact/type" type="string" calculate="&quot;person&quot;"/>
<bind nodeset="/data/contact/name" type="string" required="true()"/>
<bind nodeset="/data/contact/badge" type="string"/>
<bind nodeset="/data/contact/photo" type="binary"/>
<bind nodeset="/data/health_center_with_attachments/parent" type="string" calculate="&quot;NEW&quot;"/>
<bind nodeset="/data/health_center_with_attachments/type" type="string"/>
<bind nodeset="/data/health_center_with_attachments/contact" type="string" calculate="&quot;NEW&quot;"/>
<bind nodeset="/data/health_center_with_attachments/name" type="string" required="true()"/>
<bind nodeset="/data/health_center_with_attachments/badge" type="string"/>
<bind nodeset="/data/health_center_with_attachments/photo" type="binary"/>
<bind nodeset="/data/repeat/child/type" type="string" calculate="&quot;clinic&quot;"/>
<bind nodeset="/data/repeat/child/name" type="string" required="true()"/>
<bind nodeset="/data/repeat/child/badge" type="string"/>
<bind nodeset="/data/repeat/child/photo" type="binary"/>
<bind nodeset="/data/meta/instanceID" type="string" readonly="true()" jr:preload="uid"/>
</model>
</h:head>
<h:body>
<group ref="/data/parent">
<label ref="jr:itext('/data/parent:label')"/>
<input ref="/data/parent/name">
<label ref="jr:itext('/data/parent/name:label')"/>
</input>
<input ref="/data/parent/badge">
<label ref="jr:itext('/data/parent/badge:label')"/>
</input>
<upload ref="/data/parent/photo" mediatype="image/*">
<label ref="jr:itext('/data/parent/photo:label')"/>
</upload>
</group>
<group ref="/data/contact">
<label ref="jr:itext('/data/contact:label')"/>
<input ref="/data/contact/name">
<label ref="jr:itext('/data/contact/name:label')"/>
</input>
<input ref="/data/contact/badge">
<label ref="jr:itext('/data/contact/badge:label')"/>
</input>
<upload ref="/data/contact/photo" mediatype="image/*">
<label ref="jr:itext('/data/contact/photo:label')"/>
</upload>
</group>
<group ref="/data/health_center_with_attachments">
<label ref="jr:itext('/data/health_center_with_attachments:label')"/>
<input ref="/data/health_center_with_attachments/name">
<label ref="jr:itext('/data/health_center_with_attachments/name:label')"/>
</input>
<input ref="/data/health_center_with_attachments/badge">
<label ref="jr:itext('/data/health_center_with_attachments/badge:label')"/>
</input>
<upload ref="/data/health_center_with_attachments/photo" mediatype="image/*">
<label ref="jr:itext('/data/health_center_with_attachments/photo:label')"/>
</upload>
</group>
<group ref="/data/repeat">
<label ref="jr:itext('/data/repeat:label')"/>
<group ref="/data/repeat/child">
<label ref="jr:itext('/data/repeat/child:label')"/>
<repeat nodeset="/data/repeat/child">
<input ref="/data/repeat/child/name">
<label ref="jr:itext('/data/repeat/child/name:label')"/>
</input>
<input ref="/data/repeat/child/badge">
<label ref="jr:itext('/data/repeat/child/badge:label')"/>
</input>
<upload ref="/data/repeat/child/photo" mediatype="image/*">
<label ref="jr:itext('/data/repeat/child/photo:label')"/>
</upload>
</repeat>
</group>
</group>
</h:body>
</h:html>
Binary file not shown.
Loading
Loading