From 1850e558bd3c674a8e4bcae74bf877734deb47af Mon Sep 17 00:00:00 2001 From: Gareth Bowen Date: Thu, 7 May 2026 18:01:44 +1200 Subject: [PATCH 1/2] fix(#805): allow labels with computed media urls --- .changeset/rich-teams-push.md | 6 ++ .../select/7-images-choice-computed.xml | 59 +++++++++++++++++++ .../common/media/MediaBlockBase.vue | 1 + .../lib/reactivity/text/createTextRange.ts | 45 ++++++++++---- 4 files changed, 98 insertions(+), 13 deletions(-) create mode 100644 .changeset/rich-teams-push.md create mode 100644 packages/common/src/fixtures/select/7-images-choice-computed.xml diff --git a/.changeset/rich-teams-push.md b/.changeset/rich-teams-push.md new file mode 100644 index 000000000..98cb4d92e --- /dev/null +++ b/.changeset/rich-teams-push.md @@ -0,0 +1,6 @@ +--- +'@getodk/xforms-engine': patch +'@getodk/web-forms': patch +--- + +Fixed bug where computed image urls blocked form loading diff --git a/packages/common/src/fixtures/select/7-images-choice-computed.xml b/packages/common/src/fixtures/select/7-images-choice-computed.xml new file mode 100644 index 000000000..3a155396f --- /dev/null +++ b/packages/common/src/fixtures/select/7-images-choice-computed.xml @@ -0,0 +1,59 @@ + + + + images-choice + + + + + Reference image + jr://images/ + + + + + + + + + + + + + + + + animals-0 + tiger + tiger.jpg + + + animals-1 + camel + camel.jpg + + + + + + + + + + + + + + + + + diff --git a/packages/web-forms/src/components/common/media/MediaBlockBase.vue b/packages/web-forms/src/components/common/media/MediaBlockBase.vue index 9e0835523..70cb87cc2 100644 --- a/packages/web-forms/src/components/common/media/MediaBlockBase.vue +++ b/packages/web-forms/src/components/common/media/MediaBlockBase.vue @@ -63,6 +63,7 @@ const loadMedia = async (src?: JRResourceURL): Promise => { const setMedia = (value: string) => { mediaUrl.value = value; loading.value = false; + errorMessage.value = ''; }; const handleError = (error: string) => { diff --git a/packages/xforms-engine/src/lib/reactivity/text/createTextRange.ts b/packages/xforms-engine/src/lib/reactivity/text/createTextRange.ts index 765bf6d5b..bdb86c76f 100644 --- a/packages/xforms-engine/src/lib/reactivity/text/createTextRange.ts +++ b/packages/xforms-engine/src/lib/reactivity/text/createTextRange.ts @@ -20,29 +20,47 @@ interface ChunksAndMedia { mediaSources: MediaSources; } +const generateResourceChunk = (context: EvaluationContext, child: Element, type: ResourceType) => { + const parts = []; + for (const grandchild of child.childNodes) { + if (isElementNode(grandchild)) { + const expression = TextChunkExpression.fromOutput(grandchild); + if (expression) { + parts.push(createComputedExpression(context, expression)()); + } + } else if (isTextNode(grandchild)) { + parts.push(grandchild.data); + } + } + const url = parts.join('') as JRResourceURLString; + return TextChunkExpression.fromResource(url, type); +}; + const generateChunk = (node: Node): TextChunkExpression<'string'> | null => { if (isElementNode(node)) { return TextChunkExpression.fromOutput(node); } if (isTextNode(node)) { - const formAttribute = node.parentElement!.getAttribute('form') as ResourceType; - if (isResourceType(formAttribute)) { - return TextChunkExpression.fromResource(node.data as JRResourceURLString, formAttribute); - } return TextChunkExpression.fromLiteral(node.data); } return null; }; const generateChunksForTranslation = ( + context: EvaluationContext, textElement: Element ): Array> => { const chunks = []; - for (const child of textElement.childNodes) { - for (const grandchild of child.childNodes) { - const chunk = generateChunk(grandchild); - if (chunk) { - chunks.push(chunk); + for (const child of textElement.children) { + const formAttribute = child.getAttribute('form') as ResourceType; + if (isResourceType(formAttribute)) { + chunks.push(generateResourceChunk(context, child, formAttribute)); + } else { + for (const grandchild of child.childNodes) { + const chunk = generateChunk(grandchild); + if (chunk) { + chunks.push(chunk); + } } } } @@ -62,7 +80,7 @@ const getChunkExpressions = ( }); const lang = context.getActiveLanguage(); const elem = definition.form.model.getItextElement(lang, itextId); - return elem ? generateChunksForTranslation(elem) : []; + return elem ? generateChunksForTranslation(context, elem) : []; }; /** @@ -83,9 +101,10 @@ const createTextChunks = ( const chunkExpressions = getChunkExpressions(context, definition); chunkExpressions.forEach((chunkExpression) => { if (chunkExpression.resourceType) { - mediaSources[chunkExpression.resourceType] = JRResourceURL.from( - chunkExpression.stringValue as JRResourceURLString - ); + const url = chunkExpression.stringValue?.trim(); + if (JRResourceURL.isJRResourceReference(url)) { + mediaSources[chunkExpression.resourceType] = JRResourceURL.from(url); + } return; } From 992cee891bbda3e8eafd4eb1ba567ef8d90fce94 Mon Sep 17 00:00:00 2001 From: Gareth Bowen Date: Mon, 11 May 2026 12:26:40 +1200 Subject: [PATCH 2/2] add scenario test --- packages/scenario/test/label-media.test.ts | 108 +++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 packages/scenario/test/label-media.test.ts diff --git a/packages/scenario/test/label-media.test.ts b/packages/scenario/test/label-media.test.ts new file mode 100644 index 000000000..8130747ce --- /dev/null +++ b/packages/scenario/test/label-media.test.ts @@ -0,0 +1,108 @@ +import type { JRResourceURL } from '@getodk/common/jr-resources/JRResourceURL.ts'; +import { UnreachableError } from '@getodk/common/lib/error/UnreachableError.ts'; +import { + bind, + body, + head, + html, + input, + mainInstance, + model, + t, + title, +} from '@getodk/common/test-utils/xform-dsl/index.ts'; +import type { XFormsElement } from '@getodk/common/test-utils/xform-dsl/XFormsElement.ts'; +import type { TextRange } from '@getodk/xforms-engine'; +import { describe, expect, it } from 'vitest'; +import { Scenario } from '../src/jr/Scenario.ts'; + +describe('`