ui: file manager read pills and Edit raw (v2.7.9)
The decoded view is a read-only field table, so pressing Edit there did nothing (no readonly inputs to strip) and Save found nothing to write. - Remove the redundant Read button: the Raw/Decoded pills already read the file, so they become explicit read actions — Read raw / Read decoded (Читать сырые данные / Декодировать). They are buttons now so the server/card availability model disables them like the old Read button did. - Edit raw (Редактировать сырые данные) always edits the hex view: from decoded (or with no content loaded yet) it switches back to raw and reads the file first; the decoded view stays read-only. - Save/Cancel moved next to Edit raw (right-aligned group); the pills are inert and dimmed while editing (mode switching would discard unsaved changes). - Tests: frontend/tests/fs_edit.test.js (skipRead, edit guard, decoded auto-switch ordering, read failure abort, cancel/reset); help EN/RU.
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||
|
||||
function extractFunc(src, name) {
|
||||
const re = new RegExp('(?:async\\s+)?function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||
const m = re.exec(src);
|
||||
if (!m) throw new Error('function ' + name + ' not found');
|
||||
let i = m.index + m[0].length - 1;
|
||||
let depth = 0;
|
||||
for (; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
return src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
let code = 'var pysimFsDecodedMode = false;\n'
|
||||
+ 'var pysimFsEditMode = false;\n'
|
||||
+ 'var pysimFsEditData = null;\n'
|
||||
+ 'var pysimFsSelected = null;\n';
|
||||
for (const fn of ['pysimFsSetMode', 'pysimFsPillsEnabled', 'pysimFsEdit',
|
||||
'pysimFsCancel', 'pysimFsResetEdit']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
eval(code);
|
||||
|
||||
let events = [];
|
||||
let readCalls = 0;
|
||||
let readResult = true;
|
||||
let content, buttons, pills, editables, checkboxes, inputs;
|
||||
|
||||
function fakeClassList() {
|
||||
const set = new Set();
|
||||
return {
|
||||
add: (...c) => c.forEach(x => set.add(x)),
|
||||
remove: (...c) => c.forEach(x => set.delete(x)),
|
||||
toggle: (c, on) => { if (on) set.add(c); else set.delete(c); },
|
||||
contains: c => set.has(c),
|
||||
};
|
||||
}
|
||||
|
||||
function fakeEl() {
|
||||
return { classList: fakeClassList(), dataset: {} };
|
||||
}
|
||||
|
||||
function setup(opts) {
|
||||
opts = opts || {};
|
||||
events = [];
|
||||
readCalls = 0;
|
||||
readResult = opts.readResult !== false;
|
||||
editables = [{ removeAttribute: () => events.push('readonly-removed') }];
|
||||
checkboxes = [fakeEl()];
|
||||
inputs = [{ addEventListener: () => {} }];
|
||||
content = fakeEl();
|
||||
content.innerHTML = opts.contentHtml || '';
|
||||
content.querySelectorAll = sel => {
|
||||
if (sel === '.pysim-fs-record-cb') return checkboxes;
|
||||
if (sel.indexOf('textarea') === 0) return editables;
|
||||
return inputs;
|
||||
};
|
||||
buttons = {
|
||||
'pysim-fs-edit-btn': fakeEl(),
|
||||
'pysim-fs-save-btn': fakeEl(),
|
||||
'pysim-fs-cancel-btn': fakeEl(),
|
||||
};
|
||||
buttons['pysim-fs-save-btn'].classList.add('hidden');
|
||||
buttons['pysim-fs-cancel-btn'].classList.add('hidden');
|
||||
pills = [fakeEl(), fakeEl()];
|
||||
pills[0].dataset.mode = 'raw';
|
||||
pills[1].dataset.mode = 'dec';
|
||||
|
||||
pysimFsDecodedMode = !!opts.decoded;
|
||||
pysimFsEditMode = false;
|
||||
pysimFsEditData = null;
|
||||
pysimFsSelected = 'EF.IMSI';
|
||||
|
||||
globalThis.document = {
|
||||
getElementById: id => id === 'pysim-fs-content' ? content : (buttons[id] || null),
|
||||
querySelectorAll: sel => sel === '.pysim-mode-pill' ? pills : [],
|
||||
};
|
||||
globalThis.pysimFsRead = async () => {
|
||||
readCalls++;
|
||||
events.push('read');
|
||||
return readResult;
|
||||
};
|
||||
return {
|
||||
content,
|
||||
btn: id => buttons['pysim-fs-' + id + '-btn'],
|
||||
pill: mode => pills.find(p => p.dataset.mode === mode),
|
||||
reads: () => readCalls,
|
||||
readonlyRemoved: () => events.filter(e => e === 'readonly-removed').length,
|
||||
events,
|
||||
};
|
||||
}
|
||||
|
||||
test('Read raw / Read decoded pills switch the view and read the file', () => {
|
||||
const h = setup();
|
||||
pysimFsSetMode('raw', true);
|
||||
assert.strictEqual(pysimFsDecodedMode, false);
|
||||
assert.strictEqual(h.reads(), 0, 'skipRead updates the pills only');
|
||||
assert.ok(h.pill('raw').classList.contains('bg-blue-600'));
|
||||
assert.ok(!h.pill('dec').classList.contains('bg-blue-600'));
|
||||
|
||||
pysimFsSetMode('dec');
|
||||
assert.strictEqual(pysimFsDecodedMode, true);
|
||||
assert.strictEqual(h.reads(), 1, 'a pill click reads the selected file');
|
||||
assert.ok(h.pill('dec').classList.contains('bg-blue-600'));
|
||||
assert.ok(!h.pill('raw').classList.contains('bg-blue-600'));
|
||||
});
|
||||
|
||||
test('pill clicks are ignored while editing', () => {
|
||||
const h = setup();
|
||||
pysimFsDecodedMode = true;
|
||||
pysimFsEditMode = true;
|
||||
pysimFsSetMode('raw');
|
||||
assert.strictEqual(pysimFsDecodedMode, true, 'the view mode must not change mid-edit');
|
||||
assert.strictEqual(h.reads(), 0);
|
||||
});
|
||||
|
||||
test('Edit raw keeps a loaded raw view and enters edit mode without re-reading', async () => {
|
||||
const h = setup({ contentHtml: '<textarea readonly>aa</textarea>' });
|
||||
await pysimFsEdit();
|
||||
assert.strictEqual(h.reads(), 0);
|
||||
assert.strictEqual(pysimFsEditMode, true);
|
||||
assert.ok(h.btn('edit').classList.contains('hidden'));
|
||||
assert.ok(!h.btn('save').classList.contains('hidden'));
|
||||
assert.ok(!h.btn('cancel').classList.contains('hidden'));
|
||||
assert.strictEqual(h.readonlyRemoved(), 1);
|
||||
assert.ok(h.pill('raw').classList.contains('opacity-50'), 'pills are dimmed');
|
||||
assert.ok(h.pill('raw').classList.contains('pointer-events-none'));
|
||||
});
|
||||
|
||||
test('Edit raw switches from the decoded view to raw and reads before editing', async () => {
|
||||
const h = setup({ decoded: true });
|
||||
await pysimFsEdit();
|
||||
assert.strictEqual(pysimFsDecodedMode, false);
|
||||
assert.strictEqual(h.reads(), 1);
|
||||
assert.deepStrictEqual(h.events, ['read', 'readonly-removed'],
|
||||
'the raw view must be rendered before the readonly attributes are stripped');
|
||||
assert.ok(h.pill('raw').classList.contains('bg-blue-600'));
|
||||
assert.strictEqual(pysimFsEditMode, true);
|
||||
});
|
||||
|
||||
test('Edit raw reads a fresh selection and aborts if the read fails', async () => {
|
||||
const h = setup({ readResult: false });
|
||||
await pysimFsEdit();
|
||||
assert.strictEqual(h.reads(), 1);
|
||||
assert.strictEqual(pysimFsEditMode, false);
|
||||
assert.ok(h.btn('save').classList.contains('hidden'));
|
||||
assert.ok(!h.pill('raw').classList.contains('opacity-50'));
|
||||
});
|
||||
|
||||
test('Cancel restores the view and re-enables the pills', async () => {
|
||||
const h = setup({ contentHtml: '<textarea readonly>aa</textarea>' });
|
||||
await pysimFsEdit();
|
||||
pysimFsCancel();
|
||||
assert.strictEqual(pysimFsEditMode, false);
|
||||
assert.strictEqual(h.content.innerHTML, '<textarea readonly>aa</textarea>');
|
||||
assert.ok(!h.pill('raw').classList.contains('opacity-50'));
|
||||
assert.ok(!h.pill('raw').classList.contains('pointer-events-none'));
|
||||
assert.ok(!h.btn('edit').classList.contains('hidden'));
|
||||
assert.ok(h.btn('save').classList.contains('hidden'));
|
||||
assert.ok(h.btn('cancel').classList.contains('hidden'));
|
||||
});
|
||||
|
||||
test('Reset edit re-enables the pills', () => {
|
||||
const h = setup();
|
||||
pysimFsEditMode = true;
|
||||
pysimFsPillsEnabled(false);
|
||||
pysimFsResetEdit();
|
||||
assert.strictEqual(pysimFsEditMode, false);
|
||||
assert.ok(!h.pill('dec').classList.contains('opacity-50'));
|
||||
});
|
||||
|
||||
test('the file manager row exposes the read pills and Edit raw', () => {
|
||||
assert.ok(!html.includes('id="pysim-fs-read-btn"'), 'the redundant Read button is gone');
|
||||
assert.ok(html.includes('data-l10n="Read raw">Read raw<'));
|
||||
assert.ok(html.includes('data-l10n="Read decoded">Read decoded<'));
|
||||
assert.ok(html.includes('data-l10n="Edit raw">Edit raw<'));
|
||||
assert.ok(html.includes('id="pysim-fs-read-raw-btn"') && html.includes('data-needs="card"'));
|
||||
});
|
||||
Reference in New Issue
Block a user