Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/ninety-pets-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@getodk/xforms-engine': patch
'@getodk/scenario': patch
'@getodk/web-forms': patch
---

Set attribute value to instance value when editing submission
12 changes: 9 additions & 3 deletions packages/scenario/src/client/init.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { JRResourceService } from '@getodk/common/jr-resources/JRResourceService.ts';
import type {
EditFormInstanceInput,
FormResource,
GeolocationProvider,
InstanceAttachmentsConfig,
Expand All @@ -11,7 +12,7 @@ import type {
PreloadProperties,
RootNode,
} from '@getodk/xforms-engine';
import { createInstance } from '@getodk/xforms-engine';
import { createInstance, editInstance } from '@getodk/xforms-engine';
import type { Owner } from 'solid-js';
import { createRoot } from 'solid-js';
import { getAssertedOwner, runInSolidScope } from './solid-helpers.ts';
Expand All @@ -31,6 +32,7 @@ export interface TestFormOptions {
readonly instanceAttachments: InstanceAttachmentsConfig;
readonly preloadProperties: PreloadProperties;
readonly geolocationProvider: GeolocationProvider;
readonly editInstance: EditFormInstanceInput | null;
}

const defaultConfig = {
Expand All @@ -57,7 +59,7 @@ export const initializeTestForm = async (
const owner = getAssertedOwner();

const { formResult: form, root: instanceRoot } = await runInSolidScope(owner, async () => {
return createInstance(formResource, {
const initOptions = {
form: {
...defaultConfig,
fetchFormAttachment: options.resourceService.handleRequest,
Expand All @@ -69,7 +71,11 @@ export const initializeTestForm = async (
preloadProperties: options.preloadProperties,
geolocationProvider: options.geolocationProvider,
},
});
};
if (options.editInstance) {
return editInstance(formResource, options.editInstance, initOptions);
}
return createInstance(formResource, initOptions);
});

return {
Expand Down
8 changes: 8 additions & 0 deletions packages/scenario/src/jr/Scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { xmlElement } from '@getodk/common/test/fixtures/xform-dsl/index.ts';
import type {
AnyFormInstance,
AnyNode,
EditFormInstanceInput,
FormResource,
InstancePayload,
InstancePayloadOptions,
Expand Down Expand Up @@ -179,6 +180,7 @@ export class Scenario {
geolocationProvider: overrideOptions?.geolocationProvider ?? {
getLocation: () => Promise.resolve(''),
},
editInstance: overrideOptions?.editInstance ?? null,
};
}

Expand Down Expand Up @@ -1105,6 +1107,12 @@ export class Scenario {
});
}

async editWebFormsInstanceState(payload: EditFormInstanceInput): Promise<this> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find any current usage of this function. Would it be okay to remove it for now and add it back when it's needed? This way, the PR commit will stay focused on the relevant fixes.

const instance = await this.form.editInstance(payload, this.config.formOptions);

return this.fork(instance);
}

async restoreWebFormsInstanceState(payload: RestoreFormInstanceInput): Promise<this> {
const instance = await this.form.restoreInstance(payload, this.config.formOptions);

Expand Down
49 changes: 49 additions & 0 deletions packages/scenario/test/bind-attributes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import {
model,
repeat,
select1,
setvalue,
t,
title,
} from '@getodk/common/test/fixtures/xform-dsl/index.ts';
import { constants, type InstanceData } from '@getodk/xforms-engine';
import { beforeEach, describe, expect, it } from 'vitest';
import { stringAnswer } from '../src/answer/ExpectedStringAnswer.ts';
import { Scenario } from '../src/jr/Scenario.ts';
const { INSTANCE_FILE_NAME, INSTANCE_FILE_TYPE } = constants;

const IGNORED_INSTANCE_ID = 'ignored for purposes of functionality under test';

Expand Down Expand Up @@ -138,6 +141,52 @@ describe('Bind attributes', () => {
});
});

it('binds attributes from instance data when editing', async () => {
const instanceXML = `<data id="pets_add" version="20231204203009">
<species>cat</species>
<name>sammy</name>
<meta>
<instanceID>uuid:c0b9c932-e78b-474b-8568-48980113a7ac</instanceID>
<entity create="1" dataset="pets" id="0ed0b751-0d7d-4342-a818-67910a01df9a"><label>sammy</label></entity>
<deprecatedID>uuid:da0bb609-4293-48bd-ba64-136ffa1c43d3</deprecatedID>
</meta>
</data>`;
const instanceFile = new File([instanceXML], INSTANCE_FILE_NAME, {
type: INSTANCE_FILE_TYPE,
});
const instanceData = new FormData();
instanceData.set(INSTANCE_FILE_NAME, instanceFile);
const form = html(
head(
title('Neighborhood pet: add'),
model(
mainInstance(
t(
'data id="pets_add"',
t('species'),
t('name'),
t('meta', t('entity create="1" dataset="pets" id=""', t('label')))
)
),
bind('/data/name').type('string'),
bind('/data/meta/entity/@create').calculate('1').type('string'),
bind('/data/meta/entity/label').calculate('/data/name').type('string'),
setvalue('odk-instance-first-load', '/data/meta/entity/@id', 'uuid()')
)
),
body(input('/data/species'), input('/data/name'))
);
const scenario = await Scenario.init('upgrade form', form, {
editInstance: {
inputType: 'FORM_INSTANCE_INPUT_RESOLVED',
data: [instanceData as InstanceData],
},
});
expect(scenario.attributeOf('/data/meta/entity', 'id').getValue()).to.equal(
'0ed0b751-0d7d-4342-a818-67910a01df9a'
);
});

describe('can evaluate the attribute value', () => {
let scenario: Scenario;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
import type { StaticAttribute } from '../../integration/xpath/static-dom/StaticAttribute.ts';
import type { StaticDocument } from '../../integration/xpath/static-dom/StaticDocument.ts';
import type { StaticElement } from '../../integration/xpath/static-dom/StaticElement.ts';
import { Attribute } from '../Attribute';
import type { AnyNode } from '../hierarchy.ts';
import type { InputControl } from '../InputControl.ts';
import type { ModelValue } from '../ModelValue.ts';
import type { Note } from '../Note.ts';
import type { RangeControl } from '../RangeControl.ts';

function buildInstanceAttributeMap(
instanceNode: StaticAttribute | StaticDocument | StaticElement | null
): Map<string, StaticAttribute> {
const map = new Map<string, StaticAttribute>();
if (!instanceNode) {
return map;
}
for (const attribute of instanceNode.attributes) {
map.set(attribute.qualifiedName.getPrefixedName(), attribute);
}
return map;
}

export function buildAttributes(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
owner: AnyNode | InputControl<any> | ModelValue<any> | Note<any> | RangeControl<any>
Expand All @@ -13,7 +29,11 @@ export function buildAttributes(
if (!attributes) {
return [];
}
const instanceAttributes = buildInstanceAttributeMap(owner.instanceNode);
return Array.from(attributes.values()).map((attributeDefinition) => {
return new Attribute(owner, attributeDefinition, attributeDefinition.template);
const instanceNode =
instanceAttributes.get(attributeDefinition.qualifiedName.getPrefixedName()) ??
attributeDefinition.template;
return new Attribute(owner, attributeDefinition, instanceNode);
});
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { XFormsXPathDocument } from '../adapter/XFormsXPathNode.ts';
import type { StaticAttribute } from './StaticAttribute.ts';
import type { StaticElementOptions } from './StaticElement.ts';
import { StaticElement } from './StaticElement.ts';
import { StaticParentNode } from './StaticParentNode.ts';
Expand All @@ -15,6 +16,7 @@ export class StaticDocument extends StaticParentNode<'document'> implements XFor
readonly nodeset: string;
readonly children: readonly [root: StaticElement];
readonly childElements: readonly [root: StaticElement];
readonly attributes: readonly StaticAttribute[] = [];

constructor(options: StaticDocumentOptions) {
super('document');
Expand Down