fix(ui): herstel mobiele appacties
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 4m39s

This commit is contained in:
Ramon 2026-08-06 09:11:43 +02:00
parent 8f62df90d8
commit da9981378b
4 changed files with 86 additions and 8 deletions

View file

@ -1,3 +1,16 @@
# v0.8.19-beta — Mobiele appacties reageren weer
In het mobiele actiemenu kregen normale acties zonder expliciete
uitschakelstatus toch het HTML-attribuut `disabled`. Daardoor deed onder meer
de knop Verwijderen zichtbaar helemaal niets.
- **Verwijderen is weer klikbaar.** Alleen acties die expliciet als
uitgeschakeld zijn gemarkeerd krijgen nog het `disabled`-attribuut.
- **De actie loopt vóór het menu sluit.** Zo blijft de Alpine-scope beschikbaar
totdat de verwijdermodal daadwerkelijk is geopend.
- **Echte DOM-regressietest.** De test rendert de appkaart met Alpine en klikt
zowel de desktop- als mobiele verwijderroute volledig door.
# v0.8.18-beta — UniFi opent via HTTPS
De UniFi-container was wel correct op zijn HTTPS-poort gepubliceerd, maar

View file

@ -1 +1 @@
0.8.18-beta
0.8.19-beta

View file

@ -426,8 +426,8 @@ tailwind.config = {
<template x-for="a in stackMenuItems(s)" :key="a.id">
<button class="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-xs text-left transition"
:class="a.gevaar ? 'text-er hover:bg-er/10' : 'text-tx dark:text-tx-dark hover:bg-s2 dark:hover:bg-s2-dark'"
:disabled="a.uit" :style="a.uit ? 'opacity:.4' : ''"
@click="stackMenu = ''; a.doe()">
:disabled="a.uit === true" :style="a.uit ? 'opacity:.4' : ''"
@click="a.doe(); stackMenu = ''">
<span class="mdi text-base w-5 text-center" :class="a.icon"></span>
<span x-text="a.label"></span>
</button>

View file

@ -1,7 +1,8 @@
// Rendert de echte install-modal met Alpine in een DOM en klikt erop.
// Vangt bindingsfouten die je met losse logica niet ziet: :disabled="0"
// schakelt een knop juist UIT, want Alpine haalt een boolean-attribuut
// alleen weg bij null, undefined of false.
// Rendert de echte installatiemodal en appkaart met Alpine in een DOM en klikt
// de bedieningen daadwerkelijk aan. Vangt bindingsfouten die je met losse
// logica niet ziet: een ontbrekende eigenschap in een x-for-item liet hier het
// boolean-attribuut disabled staan. Alleen de gerenderde DOM laat vervolgens
// zien dat de knop geen klikevent meer ontvangt.
const fs = require('fs'), path = require('path');
// jsdom en Alpine zijn geen onderdeel van dit project; zonder die twee slaat
@ -38,6 +39,11 @@ w.app = function () {
c.pickIp = () => {};
c.netList = [];
c.sharedNetwork = 'serverup';
c.page = 'stacks';
c.stacks = [{
name: 'mijn-app', has_compose: true, running: true, containers: [],
categories: [], depends_missing: [], editable: true, connected: false
}];
c.modal = 'install';
c.install = {
format: 'boilerplate', loading: false, stack: 'test', repo_id: 'r', instance: 'test',
@ -55,7 +61,9 @@ w.app = function () {
};
const m = html.match(/<div x-show="modal==='install'"[\s\S]*?\n <\/div>\n/);
w.document.body.innerHTML = `<div x-data="app()">${m[0]}</div>`;
const rm = html.match(/<!-- Remove confirm -->[\s\S]*?<!-- Editor -->/);
const stacks = html.match(/<section x-show="page === 'stacks'"[\s\S]*?<\/section>/);
w.document.body.innerHTML = `<div x-data="app()">${stacks[0]}${m[0]}${rm[0]}</div>`;
w.eval(fs.readFileSync(alpinePad, 'utf8'));
const wacht = ms => new Promise(r => setTimeout(r, ms));
@ -96,6 +104,63 @@ const wacht = ms => new Promise(r => setTimeout(r, ms));
await wacht(80);
check('knop weer bruikbaar na invullen', volgende.disabled === false);
// De verwijderknop testen we via de echte Alpine @click-binding. De losse
// menu-test roept doRemove() direct aan en mist fouten in de DOM.
let verwijderVerzoek = null;
let verwijderd = false;
comp.rpc = async (url, body) => {
verwijderVerzoek = {url, body};
return {job_id: 'remove-1'};
};
comp.pollJob = (id, klaar) => klaar('done');
comp.loadStacks = async () => { verwijderd = true; };
comp.toast = () => {};
const kaartknop = [...w.document.querySelectorAll('button')].find(
b => b.getAttribute('@click') === 'confirmRemove(s.name)');
check('verwijderknop op de appkaart is gerenderd', !!kaartknop);
if (!kaartknop) {
console.log('stacks:', JSON.stringify(comp.stacks));
console.log('filtered:', JSON.stringify(comp.filteredStacks));
console.log('click-bindings:', [...w.document.querySelectorAll('button')]
.map(b => b.getAttribute('@click')).filter(Boolean).join(' | '));
process.exit(1);
}
kaartknop.click();
await wacht(60);
const verwijderModal = [...w.document.querySelectorAll('.modal')].find(
e => e.getAttribute('x-show') === "modal==='remove'");
check('verwijdermodal opent met de juiste app',
comp.modal === 'remove' && comp.removeTarget === 'mijn-app');
const veilige = [...verwijderModal.querySelectorAll('button')].find(
b => b.getAttribute('@click') === "doRemove('full')");
check('verwijderknop is actief', veilige && !veilige.disabled);
veilige.click();
await wacht(80);
check('klik verstuurt de verwijderaanvraag', verwijderVerzoek &&
verwijderVerzoek.url === '/api/stacks/mijn-app/remove' &&
verwijderVerzoek.body.mode === 'full');
check('voltooide verwijdering herlaadt de apps', verwijderd);
// Op een telefoon loopt verwijderen via een functie in het x-for-actiemenu.
comp.modal = null;
comp.removeTarget = '';
const menuActies = [...w.document.querySelectorAll('button')].filter(
b => b.getAttribute('@click') === "a.doe(); stackMenu = ''");
comp.stackMenu = 'mijn-app';
await wacht(40);
const mobielVerwijderen = menuActies.find(
b => (w.Alpine.$data(b).a || {}).id === 'remove');
check('mobiele verwijderactie is gerenderd', !!mobielVerwijderen);
if (!mobielVerwijderen) process.exit(1);
check('mobiele verwijderactie is actief',
mobielVerwijderen && !mobielVerwijderen.disabled);
mobielVerwijderen.click();
await wacht(60);
if (comp.modal !== 'remove')
console.log('mobiele status:', comp.modal, comp.removeTarget, comp.stackMenu);
check('mobiele verwijderactie opent de modal',
comp.modal === 'remove' && comp.removeTarget === 'mijn-app');
console.log(fouten === 0 ? '\nAlle DOM-controles geslaagd' : `\n${fouten} FOUTEN`);
process.exit(fouten ? 1 : 0);
})();