Add support for app string translations in questionnaire imports, enhance UI strings, and improve question navigation in editor
Some checks failed
PHPUnit / test (push) Has been cancelled

This commit is contained in:
2026-06-06 16:07:06 +02:00
parent c37940c9a9
commit 91834d0b16
3 changed files with 70 additions and 8 deletions

View File

@ -193,19 +193,44 @@ function stringSpinnerOtherEnabled(config) {
return Boolean(config.otherNextQuestionId || config.otherOptionKey);
}
function stringSpinnerNextSectionHTML(config, prefix) {
/** Question types that use config.nextQuestionId (not per-option branching). */
const QUESTION_LEVEL_NEXT_LAYOUTS = new Set([
'value_spinner',
'slider_question',
'free_text',
'date_spinner',
'multi_check_box_question',
'glass_scale_question',
'client_coach_code_question',
'client_not_signed',
'last_page',
]);
function questionNextSectionHTML(config, prefix, { label, hint } = {}) {
const next = config.nextQuestionId || '';
return `
<div class="form-group" style="margin-top:12px">
<label>Next question (list selection)</label>
<div class="form-group question-next-field" style="margin-top:12px">
<label>${label || 'Next question'}</label>
<select id="${prefix}_nextQuestionId">
<option value="">(next in list order)</option>
${branchTargetOptionsHTML(next)}
</select>
<span class="field-hint">Where to go after a normal spinner choice. <strong>Required</strong> when &ldquo;Other&rdquo; targets a question that sits next in order (so list picks can skip it).</span>
<span class="field-hint">${hint || 'Where to go after this question. Leave empty to continue in list order.'}</span>
</div>`;
}
function stringSpinnerNextSectionHTML(config, prefix) {
return questionNextSectionHTML(config, prefix, {
label: 'Next question (list selection)',
hint: 'Where to go after a normal spinner choice. <strong>Required</strong> when &ldquo;Other&rdquo; targets a question that sits next in order (so list picks can skip it).',
});
}
function readQuestionNextFromForm(prefix, config) {
const next = document.getElementById(`${prefix}_nextQuestionId`)?.value?.trim() || '';
if (next) config.nextQuestionId = next;
}
function stringSpinnerOtherSectionHTML(config, prefix) {
const enabled = stringSpinnerOtherEnabled(config);
const otherKey = config.otherOptionKey || 'other_option';
@ -288,7 +313,8 @@ function configFormHTML(layout, config, prefix) {
<div class="form-group">
<label>Extra text key (optional)</label>
<input type="text" id="${prefix}_textKey" value="${esc(config.textKey || '')}" placeholder="e.g. resilience_reflection_prompt">
</div>`;
</div>
<p class="field-hint" style="margin-top:8px">Branching: set <strong>Next question</strong> on each answer option below.</p>`;
break;
case 'multi_check_box_question':
html = `
@ -429,6 +455,9 @@ function configFormHTML(layout, config, prefix) {
</div>`;
break;
}
if (QUESTION_LEVEL_NEXT_LAYOUTS.has(layout)) {
html += questionNextSectionHTML(config, prefix);
}
return html ? `<div class="config-section">${html}</div>` : '';
}
@ -517,6 +546,9 @@ function readConfigFromForm(layout, prefix) {
if (val('textKey')) config.textKey = val('textKey');
break;
}
if (QUESTION_LEVEL_NEXT_LAYOUTS.has(layout) || layout === 'string_spinner') {
readQuestionNextFromForm(prefix, config);
}
return JSON.stringify(config);
}
@ -790,6 +822,7 @@ function showAddQuestionForm() {
<input type="number" id="aq_opt_pts" value="0" min="0" placeholder="Pts">
</div>
<div class="form-group" style="width:160px;margin-bottom:0">
<label style="font-size:.75rem;color:var(--text-secondary)">Next question</label>
<select id="aq_opt_next">
<option value="">(next in order)</option>
${branchTargetOptionsHTML()}
@ -1052,10 +1085,10 @@ function renderQuestionBody(q) {
<p><strong>Key:</strong> <code>${esc(qKey || '—')}</code> · <strong>ID:</strong> <code>${esc(localId)}</code></p>
<p><strong>German:</strong> ${esc(q.defaultText)}</p>
<p><strong>Layout:</strong> ${esc(layoutLabel(layout))}</p>
${layout === 'string_spinner' && (config.nextQuestionId || config.otherNextQuestionId)
${config.nextQuestionId || config.otherNextQuestionId
? `<p><strong>Branches:</strong>
${config.nextQuestionId ? `list → <code>${esc(config.nextQuestionId)}</code>` : 'list → (order)'}
${config.otherNextQuestionId ? ` · Other (<code>${esc(config.otherOptionKey || 'other_option')}</code>) → <code>${esc(config.otherNextQuestionId)}</code>` : ''}
${config.nextQuestionId ? `→ <code>${esc(config.nextQuestionId)}</code>` : ''}
${config.otherNextQuestionId ? `${config.nextQuestionId ? ' · ' : ''}Other (<code>${esc(config.otherOptionKey || 'other_option')}</code>) → <code>${esc(config.otherNextQuestionId)}</code>` : ''}
</p>`
: ''}
${Object.keys(config).length ? `<p><strong>Config:</strong> <code>${esc(JSON.stringify(config))}</code></p>` : ''}
@ -1265,6 +1298,7 @@ function showEditOptionForm(q, opt) {
<input type="number" id="eo_pts_${opt.answerOptionID}" value="${opt.points}" min="0">
</div>
<div class="form-group" style="flex:1;min-width:160px;margin-bottom:8px">
<label style="font-size:.8rem">Next question</label>
<select id="eo_next_${opt.answerOptionID}">
<option value="">(next in order)</option>
${qIds.map(qi => `<option value="${esc(qi.localId)}" ${opt.nextQuestionId === qi.localId ? 'selected' : ''}>${esc(branchTargetLabel(qi))}</option>`).join('')}