feat: table-align the explorer action buttons, Install green (v3.6.29)
The explorer buttons sat inline after each AID/detail line, so their columns moved with the text length. Now every entry (app / ELF) and every module line is a row with a fixed-width action column (w-28): - left cell: the text (module rows are siblings of their ELF entry, indented pl-6, so their buttons share the same column as the entry buttons); - right cell: the buttons, full width of the column, so Delete / Delete All / Make selectable / Install line up vertically across rows. All buttons go through ramActionBtn(label, action, tone): Danger (red) for Delete / Delete All, Ok (emerald) for Make selectable and the **Install** buttons, which were blue. Test pins the column width and the Install tone. 660 frontend / 504 Python green; version 3.6.29; sw simple-v302.
This commit is contained in:
+47
-26
@@ -1713,7 +1713,7 @@
|
||||
// ===== Version =====
|
||||
// Single source of truth for the PWA version: shown in the header and used
|
||||
// by the server version check in pysimConnect().
|
||||
const SIMPLE_VERSION = '3.6.28';
|
||||
const SIMPLE_VERSION = '3.6.29';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -8811,6 +8811,19 @@ function ramFmtPrivileges(hex) {
|
||||
return decoded === 'None' ? t('(none)') : decoded;
|
||||
}
|
||||
|
||||
// One explorer action button. All buttons live in a fixed-width action
|
||||
// column so Delete / Delete All / Make selectable / Install line up
|
||||
// table-like across the entry and module rows (v3.6.29).
|
||||
function ramActionBtn(label, action, tone) {
|
||||
const tones = {
|
||||
danger: 'bg-red-100 text-red-700 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400',
|
||||
ok: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400',
|
||||
};
|
||||
return '<button onclick="' + action + '" data-needs="card" class="w-full px-2 py-0.5 text-xs text-center whitespace-nowrap rounded '
|
||||
+ (tones[tone] || tones.ok)
|
||||
+ ' disabled:opacity-40 disabled:cursor-not-allowed">' + esc(label) + '</button>';
|
||||
}
|
||||
|
||||
// Render the full explorer result as structured HTML
|
||||
function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) {
|
||||
let html = '';
|
||||
@@ -8840,21 +8853,23 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) {
|
||||
html += '<div class="mb-4">';
|
||||
html += '<div class="font-semibold text-sm mb-1">' + esc(t('Applications / Applet Instances')) + '</div>';
|
||||
apps.forEach(o => {
|
||||
html += '<div class="mb-1 pl-2 border-l-2 border-green-400">';
|
||||
html += '<div>' + esc(t('Application / Instance AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?');
|
||||
if (o.aid) {
|
||||
html += ' <button onclick="ramDeleteFromExplorer(\'' + o.aid + '\', false)" data-needs="card" class="ml-2 px-2 py-0.5 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed">' + esc(t('Delete')) + '</button>';
|
||||
if ((o.lifecycle || '').toUpperCase() === '03') {
|
||||
html += ' <button onclick="ramMakeSelectableFromExplorer(\'' + o.aid + '\')" data-needs="card" class="ml-1 px-2 py-0.5 text-xs bg-emerald-100 text-emerald-700 rounded hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400 disabled:opacity-40 disabled:cursor-not-allowed">' + esc(t('Make selectable')) + '</button>';
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
html += '<div class="mb-1 pl-2 border-l-2 border-green-400 flex items-start gap-3">';
|
||||
html += '<div class="flex-1 min-w-0">';
|
||||
html += '<div>' + esc(t('Application / Instance AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?') + '</div>';
|
||||
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '') + '</div>';
|
||||
if (o.privileges) html += '<div>' + esc(t('Privileges:')) + ' ' + ramFmtPrivileges(o.privileges) + ' (' + o.privileges + ')</div>';
|
||||
if (o.implicitSel) html += '<div>' + esc(t('Implicit sel:')) + ' ' + o.implicitSel + '</div>';
|
||||
if (o.elfAid) html += '<div>' + esc(t('Load File AID / Package AID:')) + ' ' + jcAidHtml(o.elfAid) + '</div>';
|
||||
if (o.sdAid) html += '<div>' + esc(t('SD AID:')) + ' ' + jcAidHtml(o.sdAid) + '</div>';
|
||||
html += '</div>';
|
||||
html += '<div class="w-28 shrink-0 flex flex-col gap-1">';
|
||||
if (o.aid) {
|
||||
html += ramActionBtn(t('Delete'), "ramDeleteFromExplorer('" + o.aid + "', false)", 'danger');
|
||||
if ((o.lifecycle || '').toUpperCase() === '03') {
|
||||
html += ramActionBtn(t('Make selectable'), "ramMakeSelectableFromExplorer('" + o.aid + "')", 'ok');
|
||||
}
|
||||
}
|
||||
html += '</div></div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
@@ -8862,29 +8877,35 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) {
|
||||
html += '<div class="mb-4">';
|
||||
html += '<div class="font-semibold text-sm mb-1">' + esc(t('Executable Load Files (ELFs) / Packages')) + '</div>';
|
||||
elfs.forEach(o => {
|
||||
html += '<div class="mb-1 pl-2 border-l-2 border-purple-400">';
|
||||
html += '<div>' + esc(t('Load File AID / Package AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?');
|
||||
if (o.aid) {
|
||||
html += ' <button onclick="ramDeleteFromExplorer(\'' + o.aid + '\', false)" data-needs="card" class="ml-2 px-2 py-0.5 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed">' + esc(t('Delete')) + '</button>';
|
||||
html += ' <button onclick="ramDeleteFromExplorer(\'' + o.aid + '\', true)" data-needs="card" class="ml-1 px-2 py-0.5 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed">' + esc(t('Delete All')) + '</button>';
|
||||
}
|
||||
html += '</div>';
|
||||
html += '<div class="mb-1 pl-2 border-l-2 border-purple-400 flex items-start gap-3">';
|
||||
html += '<div class="flex-1 min-w-0">';
|
||||
html += '<div>' + esc(t('Load File AID / Package AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?') + '</div>';
|
||||
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '') + '</div>';
|
||||
if (o.version) html += '<div>' + esc(t('Version:')) + ' ' + o.version + '</div>';
|
||||
if (o.moduleAids && o.moduleAids.length) {
|
||||
html += '<div>' + esc(t('Executable Module AIDs / Applet Class AIDs:')) + '</div>';
|
||||
o.moduleAids.forEach(m => {
|
||||
html += '<div class="pl-4">- ' + jcAidHtml(m);
|
||||
// A loaded applet class with no instance yet: one click to
|
||||
// the single-APDU INSTALL [for install].
|
||||
if (!ramHasInstance(apps, m)) {
|
||||
html += ' <button onclick="ramInstallFromExplorer(\'' + o.aid + '\',\'' + m + '\')" data-needs="card" class="ml-2 px-2 py-0.5 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400 disabled:opacity-40 disabled:cursor-not-allowed">' + esc(t('Install')) + '</button>';
|
||||
}
|
||||
html += '</div>';
|
||||
});
|
||||
}
|
||||
if (o.sdAid) html += '<div>' + esc(t('SD AID:')) + ' ' + jcAidHtml(o.sdAid) + '</div>';
|
||||
html += '</div>';
|
||||
html += '<div class="w-28 shrink-0 flex flex-col gap-1">';
|
||||
if (o.aid) {
|
||||
html += ramActionBtn(t('Delete'), "ramDeleteFromExplorer('" + o.aid + "', false)", 'danger');
|
||||
html += ramActionBtn(t('Delete All'), "ramDeleteFromExplorer('" + o.aid + "', true)", 'danger');
|
||||
}
|
||||
html += '</div></div>';
|
||||
// Module rows: emitted as sibling rows so their Install buttons share
|
||||
// the same action column as the entry buttons.
|
||||
(o.moduleAids || []).forEach(m => {
|
||||
html += '<div class="mb-1 pl-6 flex items-start gap-3">';
|
||||
html += '<div class="flex-1 min-w-0">- ' + jcAidHtml(m) + '</div>';
|
||||
html += '<div class="w-28 shrink-0 flex flex-col gap-1">';
|
||||
// A loaded applet class with no instance yet: one click to the
|
||||
// single-APDU INSTALL [for install].
|
||||
if (!ramHasInstance(apps, m)) {
|
||||
html += ramActionBtn(t('Install'), "ramInstallFromExplorer('" + o.aid + "','" + m + "')", 'ok');
|
||||
}
|
||||
html += '</div></div>';
|
||||
});
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v301';
|
||||
const CACHE = 'simple-v302';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -25,7 +25,7 @@ function extractFunc(src, name) {
|
||||
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu',
|
||||
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
|
||||
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry',
|
||||
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges',
|
||||
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn',
|
||||
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
|
||||
let code = '';
|
||||
for (const f of FNS) {
|
||||
@@ -192,6 +192,27 @@ test('ramRenderExploreHtml puts the command format after the resource lines', ()
|
||||
assert.ok(iNv >= 0 && iV >= 0 && iFmt > iV, out);
|
||||
});
|
||||
|
||||
test('explorer action buttons share a fixed-width column, Install is green', () => {
|
||||
global.t = s => s;
|
||||
const out = ramRenderExploreHtml(
|
||||
{ appCount: 1, freeNV: 10, freeV: 5 },
|
||||
[],
|
||||
[{ aid: 'A000000151000000', lifecycle: '03', privileges: '' }],
|
||||
[{ aid: 'ELF1', lifecycle: '01', version: '1.0', moduleAids: ['M1'] }]
|
||||
);
|
||||
delete global.t;
|
||||
// every action cell is the same fixed-width column
|
||||
assert.ok(out.includes('w-28 shrink-0'), out);
|
||||
// the Install button (module without an instance) is green, not blue
|
||||
const inst = /<button onclick="ramInstallFromExplorer[^>]*class="([^"]*)"/.exec(out);
|
||||
assert.ok(inst, out);
|
||||
assert.ok(inst[1].includes('bg-emerald-100'), inst[1]);
|
||||
assert.ok(!inst[1].includes('bg-blue-'), inst[1]);
|
||||
// the delete buttons keep the danger tone
|
||||
const del = /<button onclick="ramDeleteFromExplorer[^>]*class="([^"]*)"/.exec(out);
|
||||
assert.ok(del && del[1].includes('bg-red-100'), del && del[1]);
|
||||
});
|
||||
|
||||
test('ramRenderExploreHtml localizes every label and button', () => {
|
||||
const seen = [];
|
||||
global.t = s => { seen.push(s); return 'XX' + s; };
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "3.6.28"
|
||||
version = "3.6.29"
|
||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||
requires-python = ">=3.8"
|
||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||
|
||||
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.6.28'
|
||||
VERSION = '3.6.29'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
|
||||
Reference in New Issue
Block a user