feat: next action suffix in the pending SELECT ITEM panel (v3.5.6)

The STK menu overlay's cached top menu showed the card-provided Items Next
Action Indicator (TS 102 223 8.24) as a gray suffix, but the pending
SELECT ITEM list the card returns mid-dialogue did not: the server already
attaches nai_name (via _parse_select_item), the panel just had its own row
markup without the suffix.

Both lists now render through stkMenuItemsHtml(items, handler) - identical
rows (id, text, optional gray suffix) with the per-list click callback
(stkMenuItemClick / stkSubItemClick).

stk_menu.test.js: the shared renderer (suffix only with nai_name, handler
in the row, null list) plus a call-site guard for both lists.

554 frontend / 421 Python green; version 3.5.6; sw cache simple-v259.
This commit is contained in:
2026-09-25 23:05:22 +03:00
parent 31ece16ada
commit 8064625d56
5 changed files with 42 additions and 20 deletions
+17 -17
View File
@@ -1559,7 +1559,7 @@
// ===== Version ===== // ===== Version =====
// Single source of truth for the PWA version: shown in the header and used // Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect(). // by the server version check in pysimConnect().
const SIMPLE_VERSION = '3.5.5'; const SIMPLE_VERSION = '3.5.6';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching ===== // ===== Tab switching =====
@@ -7710,19 +7710,26 @@ function stkMenuNaiSuffix(item) {
return (item && item.nai_name) ? '\u25b8 ' + item.nai_name : ''; return (item && item.nai_name) ? '\u25b8 ' + item.nai_name : '';
} }
// The overlay's item list, shared by the cached top menu and the pending
// SELECT ITEM items the card returns mid-dialogue: one row per item, each
// with the card-provided next action suffix. `handler` is the click callback
// (stkMenuItemClick / stkSubItemClick).
function stkMenuItemsHtml(items, handler) {
let html = '<div class="space-y-1">';
(items || []).forEach(item => {
const nai = stkMenuNaiSuffix(item);
html += '<div class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-slate-700 cursor-pointer" onclick="' + handler + '(' + item.id + ')"><span class="w-5 text-xs text-gray-300 dark:text-gray-600 text-right">' + item.id + '</span><span class="text-sm">' + esc(item.text) + '</span>' +
(nai ? '<span class="text-xs text-gray-400 dark:text-slate-500">' + esc(nai) + '</span>' : '') + '</div>';
});
return html + '</div>';
}
function stkMenuRenderItems() { function stkMenuRenderItems() {
const content = document.getElementById('stk-menu-panel-content'); const content = document.getElementById('stk-menu-panel-content');
content.innerHTML = '<span class="text-gray-500 text-sm">Loading...</span>'; content.innerHTML = '<span class="text-gray-500 text-sm">Loading...</span>';
pysimFetch('/api/menu').then(data => { pysimFetch('/api/menu').then(data => {
if (!data || !data.items) { content.innerHTML = '<span class="text-red-500 text-sm">No menu data</span>'; return; } if (!data || !data.items) { content.innerHTML = '<span class="text-red-500 text-sm">No menu data</span>'; return; }
let html = '<div class="space-y-1">'; content.innerHTML = stkMenuItemsHtml(data.items, 'stkMenuItemClick');
data.items.forEach(item => {
const nai = stkMenuNaiSuffix(item);
html += '<div class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-slate-700 cursor-pointer" onclick="stkMenuItemClick(' + item.id + ')"><span class="w-5 text-xs text-gray-300 dark:text-gray-600 text-right">' + item.id + '</span><span class="text-sm">' + esc(item.text) + '</span>' +
(nai ? '<span class="text-xs text-gray-400 dark:text-slate-500">' + esc(nai) + '</span>' : '') + '</div>';
});
html += '</div>';
content.innerHTML = html;
}).catch(() => { content.innerHTML = '<span class="text-red-500 text-sm">Error loading menu</span>'; }); }).catch(() => { content.innerHTML = '<span class="text-red-500 text-sm">Error loading menu</span>'; });
} }
@@ -7751,14 +7758,7 @@ function stkMenuHandleResponse(data) {
btns.classList.remove('hidden'); btns.classList.remove('hidden');
} else if (data.type === 'select_item') { } else if (data.type === 'select_item') {
stkMenuStack.push(data.items); stkMenuStack.push(data.items);
content.innerHTML = ''; content.innerHTML = stkMenuItemsHtml(data.items, 'stkSubItemClick');
const items = data.items;
let html = '<div class="space-y-1">';
items.forEach(item => {
html += '<div class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-100 dark:hover:bg-slate-700 cursor-pointer" onclick="stkSubItemClick(' + item.id + ')"><span class="w-5 text-xs text-gray-300 dark:text-gray-600 text-right">' + item.id + '</span><span class="text-sm">' + esc(item.text) + '</span></div>';
});
html += '</div>';
content.innerHTML = html;
backBtn.style.display = ''; backBtn.style.display = '';
document.getElementById('stk-ok-btn').style.display = 'none'; document.getElementById('stk-ok-btn').style.display = 'none';
btns.classList.remove('hidden'); btns.classList.remove('hidden');
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v258'; const CACHE = 'simple-v259';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+22
View File
@@ -23,6 +23,7 @@ function extractFunc(src, name, asyncFn) {
let code = extractFunc(html, 'stkMenuRespond', true) + '\n'; let code = extractFunc(html, 'stkMenuRespond', true) + '\n';
code += extractFunc(html, 'stkMenuNaiSuffix') + '\n'; code += extractFunc(html, 'stkMenuNaiSuffix') + '\n';
code += extractFunc(html, 'stkMenuItemsHtml') + '\n';
code += 'globalThis.esc = s => s;\n'; code += 'globalThis.esc = s => s;\n';
eval(code); eval(code);
@@ -47,6 +48,27 @@ test('the STK menu item suffix shows the item next action (8.24)', () => {
assert.strictEqual(stkMenuNaiSuffix(null), ''); assert.strictEqual(stkMenuNaiSuffix(null), '');
}); });
test('the overlay item list shows the next action and the row handler', () => {
const list = stkMenuItemsHtml([
{ id: 1, text: 'Menu', nai: 0x25, nai_name: 'SET UP MENU' },
{ id: 2, text: 'Info' },
], 'stkSubItemClick');
assert.match(list, /onclick="stkSubItemClick\(1\)"/);
assert.match(list, /onclick="stkSubItemClick\(2\)"/);
assert.ok(list.includes('Menu'));
assert.ok(list.includes('\u25b8 SET UP MENU'));
// the second item has no indicator -> exactly one suffix in the list
assert.strictEqual((list.match(/\u25b8/g) || []).length, 1);
assert.strictEqual(stkMenuItemsHtml(null, 'stkMenuItemClick'), '<div class="space-y-1"></div>');
});
test('both overlay lists render through the shared row helper', () => {
// the cached top menu and the pending SELECT ITEM items must use the same
// renderer, otherwise the latter silently loses the NAI suffix
assert.match(html, /stkMenuItemsHtml\(data\.items, 'stkMenuItemClick'\)/);
assert.match(html, /stkMenuItemsHtml\(data\.items, 'stkSubItemClick'\)/);
});
test('back with a fetched SELECT ITEM continues the card dialogue', async () => { test('back with a fetched SELECT ITEM continues the card dialogue', async () => {
const data = { type: 'select_item', items: [{ id: 1, text: 'Info' }] }; const data = { type: 'select_item', items: [{ id: 1, text: 'Info' }] };
const calls = setup(data); const calls = setup(data);
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "pysim-simple-server" name = "pysim-simple-server"
version = "3.5.5" version = "3.5.6"
description = "HTTP REST server wrapping pysim for the SIMple PWA" description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8" requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+1 -1
View File
@@ -29,7 +29,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad from osmocom.utils import rpad
VERSION = '3.5.5' VERSION = '3.5.6'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE