v0.8.40-beta - Filter in de app store is een menu geworden
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 13m11s
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 13m11s
- Twintig categorieknoppen en veertien labelknoppen stonden altijd uitgeklapt boven de store; bij 256 apps vulde dat een half scherm. Nu een knop Filter met een popover: categorieen in een raster van twee kolommen met kleur en aantal, labels met een eigen zoekveld, en onderaan het aantal resultaten met een wisknop. Sluit met Escape of door ernaast te klikken - De labelfilter werkte maar half: de rij was afgekapt op veertien terwijl er 297 labels zijn, dus 283 daarvan waren niet te kiezen. Het menu zoekt nu in de volledige lijst; zonder zoekterm de veertig meest gebruikte, en een gekozen label blijft zichtbaar ook als het zeldzaam is - Actieve filters staan als chips naast de knop, met een kruisje per stuk. Het zoekveld heeft een wisknop - Nieuwe test die het hele sjabloon op gebalanceerde HTML-tags controleert. Een verkeerd geplaatste </div> schuift de rest van de pagina in een andere tak en dat merk je anders pas in de browser - Nieuwe JS-tests voor de filterlogica: filteren op categorie en label, de teller, wissen, en zoeken naar een zeldzaam label Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q9eqpADJSRs49SoGGr4NAy
This commit is contained in:
parent
b54e7003e8
commit
072fdff3a2
7 changed files with 293 additions and 34 deletions
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -1,3 +1,33 @@
|
||||||
|
# v0.8.40-beta — De filter in de app store is een menu geworden
|
||||||
|
|
||||||
|
**Twee rijen knoppen vulden een half scherm.** Boven de app store stonden twintig
|
||||||
|
categorieknoppen en veertien labelknoppen, allemaal tegelijk uitgeklapt. Bij 256
|
||||||
|
apps is dat geen filter meer maar een muur waar je langs moet scrollen voordat je
|
||||||
|
de eerste app ziet.
|
||||||
|
|
||||||
|
Het is nu één knop **Filter** met een menu eronder. Categorieën staan in een
|
||||||
|
raster van twee kolommen met hun eigen kleur en het aantal apps erachter, labels
|
||||||
|
hebben een eigen zoekveld, en onderaan staat hoeveel apps er overblijven met een
|
||||||
|
knop om alles in één keer te wissen. Het menu gaat dicht als je ernaast klikt of
|
||||||
|
op Escape drukt.
|
||||||
|
|
||||||
|
**En de labelfilter werkte maar half.** De rij was afgekapt op veertien labels,
|
||||||
|
terwijl er 297 in de catalogus zitten. De overige 283 waren gewoon niet te kiezen
|
||||||
|
— je zag ze wel op de app-kaarten staan, maar erop filteren kon niet. Het menu
|
||||||
|
zoekt nu in de volledige lijst; zonder zoekterm toont het de veertig meest
|
||||||
|
gebruikte, en een label dat je gekozen hebt blijft zichtbaar ook als het zeldzaam
|
||||||
|
is.
|
||||||
|
|
||||||
|
**Wat je aan hebt staan zie je zonder het menu te openen:** actieve filters staan
|
||||||
|
als chips naast de knop, met een kruisje om ze los weg te halen. En het zoekveld
|
||||||
|
heeft een wisknop gekregen.
|
||||||
|
|
||||||
|
**Nieuwe test op de HTML zelf.** De interface is één sjabloon van ruim
|
||||||
|
drieduizend regels met Alpine-templates die in elkaar zitten. Eén `</div>`
|
||||||
|
verkeerd en de rest van de pagina schuift in de verkeerde tak — wat je pas merkt
|
||||||
|
als je die kant op klikt. Er draait nu een test die het hele sjabloon op
|
||||||
|
gebalanceerde tags controleert, naast de bestaande die de JavaScript parseert.
|
||||||
|
|
||||||
# v0.8.30-beta — Sjablonen kunnen zeggen wat ze nodig hebben, en vijftien domotica-apps
|
# v0.8.30-beta — Sjablonen kunnen zeggen wat ze nodig hebben, en vijftien domotica-apps
|
||||||
|
|
||||||
**Een app kon niet zeggen dat hij een andere nodig had.** Het `connect`-veld
|
**Een app kon niet zeggen dat hij een andere nodig had.** Het `connect`-veld
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.8.30-beta
|
0.8.40-beta
|
||||||
|
|
|
||||||
|
|
@ -423,41 +423,129 @@ tailwind.config = {
|
||||||
|
|
||||||
<!-- ══ App Store ══ -->
|
<!-- ══ App Store ══ -->
|
||||||
<section x-show="page === 'app_store'" x-cloak class="anim">
|
<section x-show="page === 'app_store'" x-cloak class="anim">
|
||||||
|
<!-- Zoeken, filtermenu en de actieve filters op één regel. Eerder stonden
|
||||||
|
hier twee rijen knoppen: twintig categorieën en veertien labels. Dat
|
||||||
|
vulde een half scherm, en van de 297 labels waren er 283 onbereikbaar
|
||||||
|
omdat de rij werd afgekapt. -->
|
||||||
<div class="flex flex-wrap items-center gap-2 mb-4">
|
<div class="flex flex-wrap items-center gap-2 mb-4">
|
||||||
<div class="relative flex-1 min-w-[200px] max-w-sm">
|
<div class="relative flex-1 min-w-[200px] max-w-sm">
|
||||||
<span class="mdi mdi-magnify absolute left-3.5 top-1/2 -translate-y-1/2 text-dm dark:text-dm-dark text-lg"></span>
|
<span class="mdi mdi-magnify absolute left-3.5 top-1/2 -translate-y-1/2 text-dm dark:text-dm-dark text-lg"></span>
|
||||||
<input x-model="storeQuery" :placeholder="t('search_apps')" class="form-input !pl-10">
|
<input x-model="storeQuery" :placeholder="t('search_apps')" class="form-input !pl-10 !pr-9">
|
||||||
|
<button type="button" x-show="storeQuery" @click="storeQuery = ''"
|
||||||
|
class="absolute right-2.5 top-1/2 -translate-y-1/2 text-dm dark:text-dm-dark hover:text-tx dark:hover:text-tx-dark"
|
||||||
|
:title="t('clear')">
|
||||||
|
<span class="mdi mdi-close-circle"></span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button @click="syncAllRepos()" class="btn"><span class="mdi mdi-source-pull"></span> <span x-text="t('pull')">Pull</span></button>
|
|
||||||
<button @click="modal='add-repo'" class="btn btn-primary"><span class="mdi mdi-plus"></span> <span x-text="t('add_repo')">Repository</span></button>
|
<!-- Filtermenu -->
|
||||||
|
<div class="relative" @keydown.escape.window="filterOpen = false">
|
||||||
|
<button type="button" class="btn !px-3" @click="filterOpen = !filterOpen"
|
||||||
|
:class="filterCount ? '!border-ac !text-ac dark:!text-ac-dark !bg-ac/5' : ''">
|
||||||
|
<span class="mdi mdi-tune-variant"></span>
|
||||||
|
<span x-text="t('filter')">Filter</span>
|
||||||
|
<span x-show="filterCount" class="badge badge-info" x-text="filterCount"></span>
|
||||||
|
<span class="mdi text-xs transition-transform"
|
||||||
|
:class="filterOpen ? 'mdi-chevron-down rotate-180' : 'mdi-chevron-down'"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div x-show="filterOpen" x-cloak @click.outside="filterOpen = false"
|
||||||
|
x-transition:enter="transition ease-out duration-150"
|
||||||
|
x-transition:enter-start="opacity-0 -translate-y-2 scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0 scale-100"
|
||||||
|
x-transition:leave="transition ease-in duration-100"
|
||||||
|
x-transition:leave-start="opacity-100 scale-100"
|
||||||
|
x-transition:leave-end="opacity-0 scale-95"
|
||||||
|
class="card shadow-pop absolute left-0 z-40 mt-2 w-[min(92vw,27rem)] origin-top p-4 space-y-4">
|
||||||
|
|
||||||
|
<!-- Categorie -->
|
||||||
|
<div x-show="storeCategories.length">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<span class="text-[10px] font-bold uppercase tracking-wider text-dm dark:text-dm-dark"
|
||||||
|
x-text="t('category')">Categorie</span>
|
||||||
|
<span class="h-px flex-1 bg-bd dark:bg-bd-dark"></span>
|
||||||
|
<button type="button" x-show="storeCat" @click="storeCat = ''"
|
||||||
|
class="text-[11px] text-t2 dark:text-t2-dark hover:text-ac"
|
||||||
|
x-text="t('all')">Alle</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- Categorieën met hun eigen kleur; klikken filtert de lijst. -->
|
<div class="grid grid-cols-2 gap-1.5">
|
||||||
<div class="flex flex-wrap gap-1.5 mb-3" x-show="storeCategories.length">
|
|
||||||
<button class="text-[11px] font-semibold px-2.5 py-1 rounded-lg border transition"
|
|
||||||
:class="!storeCat ? 'border-ac bg-ac/10 text-ac dark:text-ac-dark' : 'border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:border-bd2'"
|
|
||||||
@click="storeCat = ''" x-text="t('all')">Alle</button>
|
|
||||||
<template x-for="c in storeCategories" :key="c.id">
|
<template x-for="c in storeCategories" :key="c.id">
|
||||||
<button class="text-[11px] font-semibold px-2.5 py-1 rounded-lg border transition inline-flex items-center gap-1.5"
|
<button type="button"
|
||||||
|
class="flex items-center gap-2 px-2.5 py-1.5 rounded-xl border text-[11px] font-semibold transition text-left"
|
||||||
:style="storeCat === c.id ? catStyle(c.id) : ''"
|
:style="storeCat === c.id ? catStyle(c.id) : ''"
|
||||||
:class="storeCat === c.id ? '' : 'border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:border-bd2'"
|
:class="storeCat === c.id ? '' : 'border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:border-bd2 dark:hover:border-bd2-dark'"
|
||||||
@click="storeCat = (storeCat === c.id ? '' : c.id)">
|
@click="storeCat = (storeCat === c.id ? '' : c.id)">
|
||||||
<span class="mdi" :class="c.icon" :style="storeCat === c.id ? '' : 'color:' + c.color"></span>
|
<span class="mdi shrink-0" :class="c.icon"
|
||||||
<span x-text="c.name"></span>
|
:style="storeCat === c.id ? '' : 'color:' + c.color"></span>
|
||||||
<span class="opacity-60" x-text="c.count"></span>
|
<span class="truncate flex-1" x-text="c.name"></span>
|
||||||
|
<span class="opacity-60 tabular-nums" x-text="c.count"></span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tags uit alle repo's samen; klikken filtert de lijst. -->
|
<!-- Label. Met bijna driehonderd labels is zoeken de enige manier
|
||||||
<div class="flex flex-wrap gap-1.5 mb-5" x-show="storeTags.length">
|
om er nog iets in te vinden. -->
|
||||||
<button class="text-[11px] font-semibold px-2.5 py-1 rounded-lg border transition"
|
<div x-show="storeTags.length">
|
||||||
:class="!storeTag ? 'border-ac bg-ac/10 text-ac dark:text-ac-dark' : 'border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:border-bd2'"
|
<div class="flex items-center gap-2 mb-2">
|
||||||
@click="storeTag = ''" x-text="t('all')">Alle</button>
|
<span class="text-[10px] font-bold uppercase tracking-wider text-dm dark:text-dm-dark"
|
||||||
<template x-for="tag in storeTags" :key="tag">
|
x-text="t('tag')">Label</span>
|
||||||
<button class="text-[11px] font-mono px-2.5 py-1 rounded-lg border transition"
|
<span class="h-px flex-1 bg-bd dark:bg-bd-dark"></span>
|
||||||
:class="storeTag === tag ? 'border-ac bg-ac/10 text-ac dark:text-ac-dark' : 'border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:border-bd2'"
|
<button type="button" x-show="storeTag" @click="storeTag = ''"
|
||||||
@click="storeTag = (storeTag === tag ? '' : tag)" x-text="tag"></button>
|
class="text-[11px] text-t2 dark:text-t2-dark hover:text-ac"
|
||||||
|
x-text="t('all')">Alle</button>
|
||||||
|
</div>
|
||||||
|
<input x-model="filterTagQuery" class="form-input !h-9 !text-xs mb-2"
|
||||||
|
:placeholder="t('search_tags')">
|
||||||
|
<div class="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto pr-1">
|
||||||
|
<template x-for="tg in zichtbareTags" :key="tg.tag">
|
||||||
|
<button type="button"
|
||||||
|
class="text-[11px] font-mono px-2.5 py-1 rounded-lg border transition inline-flex items-center gap-1.5"
|
||||||
|
:class="storeTag === tg.tag ? 'border-ac bg-ac/10 text-ac dark:text-ac-dark' : 'border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:border-bd2 dark:hover:border-bd2-dark'"
|
||||||
|
@click="storeTag = (storeTag === tg.tag ? '' : tg.tag)">
|
||||||
|
<span x-text="tg.tag"></span>
|
||||||
|
<span class="opacity-50 tabular-nums" x-text="tg.count"></span>
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
<p x-show="!zichtbareTags.length" class="text-[11px] text-dm dark:text-dm-dark py-1"
|
||||||
|
x-text="t('no_results')">Niets gevonden</p>
|
||||||
|
</div>
|
||||||
|
<p x-show="verborgenTags > 0" class="text-[10px] text-dm dark:text-dm-dark mt-1.5"
|
||||||
|
x-text="t('more_tags', {n: verborgenTags})"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Wat het oplevert, en alles in één keer weg -->
|
||||||
|
<div class="flex items-center gap-2 pt-1 border-t border-bd dark:border-bd-dark">
|
||||||
|
<span class="text-[11px] text-t2 dark:text-t2-dark"
|
||||||
|
x-text="t('n_apps_found', {n: storeResultaat})"></span>
|
||||||
|
<button type="button" class="btn-sm ml-auto" x-show="filterCount"
|
||||||
|
@click="wisFilters()">
|
||||||
|
<span class="mdi mdi-filter-remove-outline"></span>
|
||||||
|
<span x-text="t('clear_filters')">Wissen</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Wat er aan staat, ook zonder het menu te openen -->
|
||||||
|
<template x-if="storeCat">
|
||||||
|
<button type="button" class="text-[11px] font-semibold px-2.5 py-1.5 rounded-lg border inline-flex items-center gap-1.5"
|
||||||
|
:style="catStyle(storeCat)" @click="storeCat = ''">
|
||||||
|
<span class="mdi" :class="cat(storeCat).icon"></span>
|
||||||
|
<span x-text="cat(storeCat).name"></span>
|
||||||
|
<span class="mdi mdi-close opacity-70"></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template x-if="storeTag">
|
||||||
|
<button type="button" @click="storeTag = ''"
|
||||||
|
class="text-[11px] font-mono px-2.5 py-1.5 rounded-lg border border-ac bg-ac/10 text-ac dark:text-ac-dark inline-flex items-center gap-1.5">
|
||||||
|
<span x-text="storeTag"></span>
|
||||||
|
<span class="mdi mdi-close opacity-70"></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<button @click="syncAllRepos()" class="btn ml-auto"><span class="mdi mdi-source-pull"></span> <span x-text="t('pull')">Pull</span></button>
|
||||||
|
<button @click="modal='add-repo'" class="btn btn-primary"><span class="mdi mdi-plus"></span> <span x-text="t('add_repo')">Repository</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-7">
|
<div class="space-y-7">
|
||||||
<template x-for="repo in repos" :key="repo.id">
|
<template x-for="repo in repos" :key="repo.id">
|
||||||
|
|
@ -2156,6 +2244,7 @@ function app() {
|
||||||
stacks: [],
|
stacks: [],
|
||||||
stackQuery: '', stackCat: '',
|
stackQuery: '', stackCat: '',
|
||||||
storeQuery: '', storeTag: '', storeCat: '',
|
storeQuery: '', storeTag: '', storeCat: '',
|
||||||
|
filterOpen: false, filterTagQuery: '',
|
||||||
categories: [], catMap: {},
|
categories: [], catMap: {},
|
||||||
images: [],
|
images: [],
|
||||||
audit: [], auditTotal: 0, auditFacets: {}, auditTimer: null,
|
audit: [], auditTotal: 0, auditFacets: {}, auditTimer: null,
|
||||||
|
|
@ -2509,8 +2598,10 @@ function app() {
|
||||||
|| (s.tags || []).some(t => String(t).toLowerCase().includes(q));
|
|| (s.tags || []).some(t => String(t).toLowerCase().includes(q));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// Alle tags uit alle repo's, op frequentie gesorteerd zodat de bruikbaarste
|
// Alle labels uit alle repo's met hun aantal, op frequentie gesorteerd
|
||||||
// filters vooraan staan.
|
// zodat de bruikbaarste vooraan staan. Dit was afgekapt op veertien, wat bij
|
||||||
|
// 297 labels betekende dat je de rest niet kon kiezen; het menu zoekt nu in
|
||||||
|
// de volledige lijst.
|
||||||
get storeTags() {
|
get storeTags() {
|
||||||
const telling = {};
|
const telling = {};
|
||||||
for (const lijst of Object.values(this.storeStacks || {})) {
|
for (const lijst of Object.values(this.storeStacks || {})) {
|
||||||
|
|
@ -2520,7 +2611,38 @@ function app() {
|
||||||
}
|
}
|
||||||
return Object.keys(telling)
|
return Object.keys(telling)
|
||||||
.sort((a, b) => telling[b] - telling[a] || a.localeCompare(b))
|
.sort((a, b) => telling[b] - telling[a] || a.localeCompare(b))
|
||||||
.slice(0, 14);
|
.map(tag => ({ tag, count: telling[tag] }));
|
||||||
|
},
|
||||||
|
// Wat er in het menu past: de zoekterm eroverheen, en anders de eerste
|
||||||
|
// veertig. Alles tonen maakt van het menu weer een muur.
|
||||||
|
get zichtbareTags() {
|
||||||
|
const q = this.filterTagQuery.trim().toLowerCase();
|
||||||
|
const lijst = this.storeTags;
|
||||||
|
if (q) return lijst.filter(t => t.tag.toLowerCase().includes(q)).slice(0, 60);
|
||||||
|
// Het gekozen label hoort zichtbaar te blijven, ook als het zeldzaam is.
|
||||||
|
const top = lijst.slice(0, 40);
|
||||||
|
if (this.storeTag && !top.some(t => t.tag === this.storeTag)) {
|
||||||
|
const gekozen = lijst.find(t => t.tag === this.storeTag);
|
||||||
|
if (gekozen) top.unshift(gekozen);
|
||||||
|
}
|
||||||
|
return top;
|
||||||
|
},
|
||||||
|
get verborgenTags() {
|
||||||
|
if (this.filterTagQuery.trim()) return 0;
|
||||||
|
return Math.max(0, this.storeTags.length - this.zichtbareTags.length);
|
||||||
|
},
|
||||||
|
get filterCount() {
|
||||||
|
return (this.storeCat ? 1 : 0) + (this.storeTag ? 1 : 0);
|
||||||
|
},
|
||||||
|
// Hoeveel apps er over blijven met de huidige keuze, over alle repo's heen.
|
||||||
|
get storeResultaat() {
|
||||||
|
return Object.keys(this.storeStacks || {})
|
||||||
|
.reduce((n, id) => n + this.filteredStore(id).length, 0);
|
||||||
|
},
|
||||||
|
wisFilters() {
|
||||||
|
this.storeCat = '';
|
||||||
|
this.storeTag = '';
|
||||||
|
this.filterTagQuery = '';
|
||||||
},
|
},
|
||||||
|
|
||||||
get filteredStacks() {
|
get filteredStacks() {
|
||||||
|
|
|
||||||
|
|
@ -350,5 +350,13 @@
|
||||||
"appdata_dir": "Appdata directory",
|
"appdata_dir": "Appdata directory",
|
||||||
"depends_on_missing": "This app still needs something",
|
"depends_on_missing": "This app still needs something",
|
||||||
"depends_on_install": "Install first",
|
"depends_on_install": "Install first",
|
||||||
"depends_on_not_in_store": "'{app}' is not in the store"
|
"depends_on_not_in_store": "'{app}' is not in the store",
|
||||||
|
"filter": "Filter",
|
||||||
|
"category": "Category",
|
||||||
|
"tag": "Tag",
|
||||||
|
"search_tags": "Search tags…",
|
||||||
|
"clear_filters": "Clear",
|
||||||
|
"more_tags": "and {n} more — type to search",
|
||||||
|
"n_apps_found": "{n} apps",
|
||||||
|
"clear": "Clear"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -350,5 +350,13 @@
|
||||||
"appdata_dir": "Appdata-map",
|
"appdata_dir": "Appdata-map",
|
||||||
"depends_on_missing": "Deze app heeft nog iets nodig",
|
"depends_on_missing": "Deze app heeft nog iets nodig",
|
||||||
"depends_on_install": "Eerst installeren",
|
"depends_on_install": "Eerst installeren",
|
||||||
"depends_on_not_in_store": "'{app}' staat niet in de store"
|
"depends_on_not_in_store": "'{app}' staat niet in de store",
|
||||||
|
"filter": "Filter",
|
||||||
|
"category": "Categorie",
|
||||||
|
"tag": "Label",
|
||||||
|
"search_tags": "Label zoeken…",
|
||||||
|
"clear_filters": "Wissen",
|
||||||
|
"more_tags": "en nog {n} andere — typ om te zoeken",
|
||||||
|
"n_apps_found": "{n} apps",
|
||||||
|
"clear": "Wissen"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,51 @@ check('één groep: geen kop', c.stepGroups('basis')[0].showHeader === false);
|
||||||
c.install.fields.push(veld('c','basis',{group:'Tweede'}));
|
c.install.fields.push(veld('c','basis',{group:'Tweede'}));
|
||||||
check('twee groepen: wel koppen', c.stepGroups('basis').every(g => g.showHeader));
|
check('twee groepen: wel koppen', c.stepGroups('basis').every(g => g.showHeader));
|
||||||
|
|
||||||
|
// ── Filtermenu van de app store ─────────────────────────────────────────────
|
||||||
|
// De labelrij was afgekapt op veertien, waardoor je de rest niet kon kiezen. Het
|
||||||
|
// menu zoekt nu in de volledige lijst.
|
||||||
|
c.storeStacks = { r: [
|
||||||
|
{dir:'sonarr', name:'Sonarr', categories:['media'], tags:['media','series']},
|
||||||
|
{dir:'radarr', name:'Radarr', categories:['media'], tags:['media','films']},
|
||||||
|
{dir:'loki', name:'Loki', categories:['monitoring'], tags:['logs']},
|
||||||
|
{dir:'zeldzaam',name:'Zeldzaam',categories:['overig'], tags:['heelzeldzaam']},
|
||||||
|
]};
|
||||||
|
c.categories = [
|
||||||
|
{id:'media', name:'Media', color:'#8b5cf6', icon:'mdi-play'},
|
||||||
|
{id:'monitoring', name:'Monitoring', color:'#f97316', icon:'mdi-chart-line'},
|
||||||
|
{id:'overig', name:'Overig', color:'#94a3b8', icon:'mdi-shape'},
|
||||||
|
];
|
||||||
|
c.storeQuery = ''; c.storeCat = ''; c.storeTag = ''; c.filterTagQuery = '';
|
||||||
|
|
||||||
|
check('geen filter: alles telt mee', c.storeResultaat === 4);
|
||||||
|
check('geen actieve filters', c.filterCount === 0);
|
||||||
|
check('labels hebben een aantal',
|
||||||
|
c.storeTags[0].tag === 'media' && c.storeTags[0].count === 2);
|
||||||
|
check('alle labels blijven beschikbaar', c.storeTags.length === 5);
|
||||||
|
|
||||||
|
c.storeCat = 'media';
|
||||||
|
check('categorie filtert', c.storeResultaat === 2);
|
||||||
|
check('teller telt de categorie', c.filterCount === 1);
|
||||||
|
c.storeTag = 'films';
|
||||||
|
check('categorie en label samen', c.storeResultaat === 1);
|
||||||
|
check('teller telt allebei', c.filterCount === 2);
|
||||||
|
|
||||||
|
c.wisFilters();
|
||||||
|
check('wissen zet alles terug', c.storeResultaat === 4 && c.filterCount === 0);
|
||||||
|
|
||||||
|
c.filterTagQuery = 'zeld';
|
||||||
|
check('zoeken vindt een zeldzaam label',
|
||||||
|
c.zichtbareTags.length === 1 && c.zichtbareTags[0].tag === 'heelzeldzaam');
|
||||||
|
check('bij zoeken niets verborgen', c.verborgenTags === 0);
|
||||||
|
c.filterTagQuery = 'bestaatniet';
|
||||||
|
check('geen treffers geeft een lege lijst', c.zichtbareTags.length === 0);
|
||||||
|
|
||||||
|
// Een gekozen label mag niet uit beeld vallen omdat het zeldzaam is.
|
||||||
|
c.filterTagQuery = '';
|
||||||
|
c.storeTag = 'heelzeldzaam';
|
||||||
|
check('gekozen label blijft zichtbaar',
|
||||||
|
c.zichtbareTags.some(t => t.tag === 'heelzeldzaam'));
|
||||||
|
|
||||||
// ── Afhankelijkheden ────────────────────────────────────────────────────────
|
// ── Afhankelijkheden ────────────────────────────────────────────────────────
|
||||||
// De melding hoort alleen te verschijnen voor wat er níét draait, en te
|
// De melding hoort alleen te verschijnen voor wat er níét draait, en te
|
||||||
// verdwijnen zodra het er wel is.
|
// verdwijnen zodra het er wel is.
|
||||||
|
|
|
||||||
|
|
@ -47,3 +47,49 @@ def test_javascript_van_de_pagina_parseert():
|
||||||
r = subprocess.run(["node", "--check", "-"], input=grootste,
|
r = subprocess.run(["node", "--check", "-"], input=grootste,
|
||||||
capture_output=True, text=True, timeout=30)
|
capture_output=True, text=True, timeout=30)
|
||||||
assert r.returncode == 0, r.stderr
|
assert r.returncode == 0, r.stderr
|
||||||
|
|
||||||
|
|
||||||
|
def test_de_html_van_de_pagina_is_gebalanceerd():
|
||||||
|
"""Een tag die niet dichtgaat maakt de pagina stil kapot.
|
||||||
|
|
||||||
|
De hele interface is één sjabloon van ruim drieduizend regels met Alpine-
|
||||||
|
templates die in elkaar zitten. Zet je bij het bewerken één `</div>`
|
||||||
|
verkeerd, dan schuift de rest van de pagina in de verkeerde tak en zie je
|
||||||
|
dat pas in de browser — als je die kant op klikt.
|
||||||
|
"""
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
|
||||||
|
# Tags die geen sluiting hebben; svg-vormen zitten erbij omdat de iconen
|
||||||
|
# als inline svg meekomen.
|
||||||
|
LEEG = {"input", "br", "img", "hr", "meta", "link", "source", "area", "col",
|
||||||
|
"embed", "track", "wbr", "path", "circle", "rect", "line",
|
||||||
|
"polyline", "polygon", "use"}
|
||||||
|
|
||||||
|
class Balans(HTMLParser):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.stapel = []
|
||||||
|
self.fouten = []
|
||||||
|
|
||||||
|
def handle_starttag(self, tag, attrs):
|
||||||
|
if tag not in LEEG:
|
||||||
|
self.stapel.append((tag, self.getpos()[0]))
|
||||||
|
|
||||||
|
def handle_endtag(self, tag):
|
||||||
|
if tag in LEEG:
|
||||||
|
return
|
||||||
|
if not self.stapel:
|
||||||
|
self.fouten.append(f"regel {self.getpos()[0]}: </{tag}> zonder opening")
|
||||||
|
elif self.stapel[-1][0] != tag:
|
||||||
|
open_tag, regel = self.stapel[-1]
|
||||||
|
self.fouten.append(
|
||||||
|
f"regel {self.getpos()[0]}: </{tag}> sluit <{open_tag}> "
|
||||||
|
f"van regel {regel} af")
|
||||||
|
else:
|
||||||
|
self.stapel.pop()
|
||||||
|
|
||||||
|
p = Balans()
|
||||||
|
p.feed((WORTEL / "server-up" / "templates" / "index.html")
|
||||||
|
.read_text(encoding="utf-8"))
|
||||||
|
assert not p.fouten, p.fouten[:5]
|
||||||
|
assert not p.stapel, [f"<{t}> op regel {r} gaat nooit dicht" for t, r in p.stapel[:5]]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue