Skip to content

Commit 81a57c3

Browse files
authored
feat(#243): Adds the rank question type (#288)
feat: Add Rank support in xform-engine and Web Forms - Introduces Rank support in xform-engine and Web Forms - Implements Rank component using vue-draggable-plus - Integrates Rank handling between xform-engine and Web Forms - Adds demo forms, test coverage, and choice filter support - Improves typing, refactors code, and simplifies API - Enhances UI consistency with Select and refines button behavior - Adds hold-and-drag support for touch events ### Testing Tested in: Chrome, Firefox, Safari, Chrome for Android, and responsive design.
1 parent 11a86ef commit 81a57c3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2296
-1752
lines changed

.changeset/proud-beans-march.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@getodk/xforms-engine': minor
3+
'@getodk/web-forms': minor
4+
'@getodk/scenario': minor
5+
'@getodk/common': patch
6+
---
7+
8+
Support for rank question types (`<odk:rank>`)
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?xml version="1.0"?>
2+
<h:html xmlns="http://www.w3.org/2002/xforms" xmlns:h="http://www.w3.org/1999/xhtml"
3+
xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
4+
xmlns:jr="http://openrosa.org/javarosa" xmlns:orx="http://openrosa.org/xforms"
5+
xmlns:odk="http://www.opendatakit.org/xforms">
6+
<h:head>
7+
<h:title>Rank</h:title>
8+
<model odk:xforms-version="1.0.0">
9+
<itext>
10+
<translation lang="English (en)">
11+
<text id="decision_making-0">
12+
<value>Health</value>
13+
</text>
14+
<text id="decision_making-1">
15+
<value>Family and Friends</value>
16+
</text>
17+
<text id="decision_making-2">
18+
<value>Career Growth and Learning Opportunities</value>
19+
</text>
20+
<text id="decision_making-3">
21+
<value>Financial Stability</value>
22+
</text>
23+
<text id="decision_making-4">
24+
<value>Pursuit of Hobbies and Passions</value>
25+
</text>
26+
<text id="decision_making-5">
27+
<value>Environmental Sustainability</value>
28+
</text>
29+
<text id="decision_making-6">
30+
<value>Time Management and Work-Life Balance</value>
31+
</text>
32+
<text id="decision_making-7">
33+
<value>Building a Supportive Community</value>
34+
</text>
35+
<text id="decision_making-8">
36+
<value>Personal Development and Mindfulness</value>
37+
</text>
38+
<text id="decision_making-9">
39+
<value>Creativity and Innovation</value>
40+
</text>
41+
<text id="/data/priorities:label">
42+
<value>What values guide your decision-making?</value>
43+
</text>
44+
</translation>
45+
<translation lang="French (fr)">
46+
<text id="decision_making-0">
47+
<value>Santé</value>
48+
</text>
49+
<text id="decision_making-1">
50+
<value>Famille et amis</value>
51+
</text>
52+
<text id="decision_making-2">
53+
<value>Croissance professionnelle et opportunités d'apprentissage</value>
54+
</text>
55+
<text id="decision_making-3">
56+
<value>Stabilité financière</value>
57+
</text>
58+
<text id="decision_making-4">
59+
<value>Poursuite de loisirs et passions</value>
60+
</text>
61+
<text id="decision_making-5">
62+
<value>Durabilité environnementale</value>
63+
</text>
64+
<text id="decision_making-6">
65+
<value>Gestion du temps et équilibre vie professionnelle/vie personnelle</value>
66+
</text>
67+
<text id="decision_making-7">
68+
<value>Construire une communauté solidaire</value>
69+
</text>
70+
<text id="decision_making-8">
71+
<value>Développement personnel et pleine conscience</value>
72+
</text>
73+
<text id="decision_making-9">
74+
<value>Créativité et innovation</value>
75+
</text>
76+
<text id="/data/priorities:label">
77+
<value>Quelles valeurs guident votre prise de décision?</value>
78+
</text>
79+
</translation>
80+
</itext>
81+
<instance>
82+
<data id="1_rank" version="2025011401">
83+
<priorities/>
84+
<meta>
85+
<instanceID/>
86+
</meta>
87+
</data>
88+
</instance>
89+
<instance id="decision_making">
90+
<root>
91+
<item>
92+
<itextId>decision_making-0</itextId>
93+
<name>health</name>
94+
</item>
95+
<item>
96+
<itextId>decision_making-1</itextId>
97+
<name>family_and_friends</name>
98+
</item>
99+
<item>
100+
<itextId>decision_making-2</itextId>
101+
<name>career_growth_and_learning_opportunities</name>
102+
</item>
103+
<item>
104+
<itextId>decision_making-3</itextId>
105+
<name>financial_stability</name>
106+
</item>
107+
<item>
108+
<itextId>decision_making-4</itextId>
109+
<name>pursuit_of_hobbies_and_passions</name>
110+
</item>
111+
<item>
112+
<itextId>decision_making-5</itextId>
113+
<name>environmental_sustainability</name>
114+
</item>
115+
<item>
116+
<itextId>decision_making-6</itextId>
117+
<name>time_management_and_work_life_balance</name>
118+
</item>
119+
<item>
120+
<itextId>decision_making-7</itextId>
121+
<name>building_a_supportive_community</name>
122+
</item>
123+
<item>
124+
<itextId>decision_making-8</itextId>
125+
<name>personal_development_and_mindfulness</name>
126+
</item>
127+
<item>
128+
<itextId>decision_making-9</itextId>
129+
<name>creativity_and_innovation</name>
130+
</item>
131+
</root>
132+
</instance>
133+
<bind nodeset="/data/priorities" type="odk:rank"/>
134+
<bind nodeset="/data/meta/instanceID" type="string" readonly="true()" jr:preload="uid"/>
135+
</model>
136+
</h:head>
137+
<h:body>
138+
<odk:rank ref="/data/priorities">
139+
<label ref="jr:itext('/data/priorities:label')"/>
140+
<itemset nodeset="randomize(instance('decision_making')/root/item)">
141+
<value ref="name"/>
142+
<label ref="jr:itext(itextId)"/>
143+
</itemset>
144+
</odk:rank>
145+
</h:body>
146+
</h:html>
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?xml version="1.0"?>
2+
<h:html xmlns="http://www.w3.org/2002/xforms" xmlns:h="http://www.w3.org/1999/xhtml"
3+
xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
4+
xmlns:jr="http://openrosa.org/javarosa" xmlns:orx="http://openrosa.org/xforms"
5+
xmlns:odk="http://www.opendatakit.org/xforms">
6+
<h:head>
7+
<h:title>Rank with choice filter</h:title>
8+
<model odk:xforms-version="1.0.0">
9+
<itext>
10+
<translation lang="English (en)">
11+
<text id="decision_making-0">
12+
<value>Health</value>
13+
</text>
14+
<text id="decision_making-1">
15+
<value>Family and Friends</value>
16+
</text>
17+
<text id="decision_making-2">
18+
<value>Career Growth and Learning Opportunities</value>
19+
</text>
20+
<text id="decision_making-3">
21+
<value>Financial Stability</value>
22+
</text>
23+
<text id="decision_making-4">
24+
<value>Pursuit of Hobbies and Passions</value>
25+
</text>
26+
<text id="decision_making-5">
27+
<value>Environmental Sustainability</value>
28+
</text>
29+
<text id="decision_making-6">
30+
<value>Time Management and Work-Life Balance</value>
31+
</text>
32+
<text id="decision_making-7">
33+
<value>Building a Supportive Community</value>
34+
</text>
35+
<text id="decision_making-8">
36+
<value>Personal Development and Mindfulness</value>
37+
</text>
38+
<text id="decision_making-9">
39+
<value>Creativity and Innovation</value>
40+
</text>
41+
<text id="/data/decision_making:label">
42+
<value>Choose the values that guide your decision-making</value>
43+
</text>
44+
<text id="/data/priorities:label">
45+
<value>Please prioritize the values that guide your decision-making</value>
46+
</text>
47+
</translation>
48+
<translation lang="French (fr)">
49+
<text id="decision_making-0">
50+
<value>Santé</value>
51+
</text>
52+
<text id="decision_making-1">
53+
<value>Famille et amis</value>
54+
</text>
55+
<text id="decision_making-2">
56+
<value>Croissance professionnelle et opportunités d'apprentissage</value>
57+
</text>
58+
<text id="decision_making-3">
59+
<value>Stabilité financière</value>
60+
</text>
61+
<text id="decision_making-4">
62+
<value>Poursuite de loisirs et passions</value>
63+
</text>
64+
<text id="decision_making-5">
65+
<value>Durabilité environnementale</value>
66+
</text>
67+
<text id="decision_making-6">
68+
<value>Gestion du temps et équilibre vie professionnelle/vie personnelle</value>
69+
</text>
70+
<text id="decision_making-7">
71+
<value>Construire une communauté solidaire</value>
72+
</text>
73+
<text id="decision_making-8">
74+
<value>Développement personnel et pleine conscience</value>
75+
</text>
76+
<text id="decision_making-9">
77+
<value>Créativité et innovation</value>
78+
</text>
79+
<text id="/data/decision_making:label">
80+
<value>Choisissez les valeurs qui guident votre prise de décision</value>
81+
</text>
82+
<text id="/data/priorities:label">
83+
<value>Veuillez prioriser les valeurs qui guident votre prise de décision</value>
84+
</text>
85+
</translation>
86+
</itext>
87+
<instance>
88+
<data id="1_rank_with_choice_filter" version="2025011401">
89+
<decision_making/>
90+
<priorities/>
91+
<meta>
92+
<instanceID/>
93+
</meta>
94+
</data>
95+
</instance>
96+
<instance id="decision_making">
97+
<root>
98+
<item>
99+
<itextId>decision_making-0</itextId>
100+
<name>health</name>
101+
</item>
102+
<item>
103+
<itextId>decision_making-1</itextId>
104+
<name>family_and_friends</name>
105+
</item>
106+
<item>
107+
<itextId>decision_making-2</itextId>
108+
<name>career_growth_and_learning_opportunities</name>
109+
</item>
110+
<item>
111+
<itextId>decision_making-3</itextId>
112+
<name>financial_stability</name>
113+
</item>
114+
<item>
115+
<itextId>decision_making-4</itextId>
116+
<name>pursuit_of_hobbies_and_passions</name>
117+
</item>
118+
<item>
119+
<itextId>decision_making-5</itextId>
120+
<name>environmental_sustainability</name>
121+
</item>
122+
<item>
123+
<itextId>decision_making-6</itextId>
124+
<name>time_management_and_work_life_balance</name>
125+
</item>
126+
<item>
127+
<itextId>decision_making-7</itextId>
128+
<name>building_a_supportive_community</name>
129+
</item>
130+
<item>
131+
<itextId>decision_making-8</itextId>
132+
<name>personal_development_and_mindfulness</name>
133+
</item>
134+
<item>
135+
<itextId>decision_making-9</itextId>
136+
<name>creativity_and_innovation</name>
137+
</item>
138+
</root>
139+
</instance>
140+
<bind nodeset="/data/decision_making" type="string" required="true()"/>
141+
<bind nodeset="/data/priorities" type="odk:rank" required="true()"
142+
relevant="count-selected( /data/decision_making )&gt;0"/>
143+
<bind nodeset="/data/meta/instanceID" type="string" readonly="true()" jr:preload="uid"/>
144+
</model>
145+
</h:head>
146+
<h:body>
147+
<select ref="/data/decision_making">
148+
<label ref="jr:itext('/data/decision_making:label')"/>
149+
<itemset nodeset="instance('decision_making')/root/item">
150+
<value ref="name"/>
151+
<label ref="jr:itext(itextId)"/>
152+
</itemset>
153+
</select>
154+
<odk:rank ref="/data/priorities">
155+
<label ref="jr:itext('/data/priorities:label')"/>
156+
<itemset nodeset="instance('decision_making')/root/item[selected( /data/decision_making , name)]">
157+
<value ref="name"/>
158+
<label ref="jr:itext(itextId)"/>
159+
</itemset>
160+
</odk:rank>
161+
</h:body>
162+
</h:html>

packages/common/src/test/fixtures/xform-dsl/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,22 @@ export const proposed_selectDynamic: Proposed_selectDynamic = (
218218

219219
export { proposed_selectDynamic as selectDynamic };
220220

221+
export const proposed_rankDynamic = (ref: string, nodesetRef: string): XFormsElement => {
222+
const value = t('value ref="value"');
223+
const label = t('label ref="label"');
224+
225+
const itemsetAttributes = new Map<string, string>();
226+
itemsetAttributes.set('nodeset', nodesetRef);
227+
228+
const itemset = new TagXFormsElement('itemset', itemsetAttributes, [value, label]);
229+
const rankAttributes = new Map<string, string>();
230+
rankAttributes.set('ref', ref);
231+
232+
return new TagXFormsElement('odk:rank', rankAttributes, [itemset]) as XFormsElement;
233+
};
234+
235+
export { proposed_rankDynamic as rankDynamic };
236+
221237
export const group = (ref: string, ...children: XFormsElement[]): XFormsElement => {
222238
return t(`group ref="${ref}"`, ...children);
223239
};

packages/common/types/JSONValue.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export type JSONValue =
1111
export type JSONArray = readonly JSONValue[];
1212

1313
// This must be an interface to avoid a circular type reference error
14-
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
1514
export interface JSONObject {
1615
readonly [key: string]: JSONValue;
1716
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { JSONValue } from '@getodk/common/types/JSONValue.ts';
2+
import type { RankNode } from '@getodk/xforms-engine';
3+
import { ValueNodeAnswer } from './ValueNodeAnswer.ts';
4+
5+
export class RankNodeAnswer extends ValueNodeAnswer<RankNode> {
6+
readonly stringValue: string;
7+
readonly value: readonly string[];
8+
9+
constructor(node: RankNode) {
10+
super(node);
11+
12+
this.stringValue = node.currentState.instanceValue;
13+
this.value = node.currentState.value;
14+
}
15+
16+
override inspectValue(): JSONValue {
17+
return this.stringValue;
18+
}
19+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { JSONValue } from '@getodk/common/types/JSONValue.ts';
2+
import type { RankNode } from '@getodk/xforms-engine';
3+
import { ComparableAnswer } from './ComparableAnswer.ts';
4+
5+
/**
6+
* Produces a value which may be **assigned** to a {@link RankNode}, e.g.
7+
* as part of a test's "act" phase.
8+
*/
9+
export class RankValuesAnswer extends ComparableAnswer {
10+
readonly stringValue: string;
11+
12+
constructor(readonly values: readonly string[]) {
13+
super();
14+
15+
this.stringValue = values.join(' ');
16+
}
17+
18+
override inspectValue(): JSONValue {
19+
return this.values;
20+
}
21+
}

0 commit comments

Comments
 (0)