update
This commit is contained in:
@@ -5,7 +5,13 @@ import SeitenLeiste from './components/SeitenLeiste.vue'
|
||||
import FeatureListe from './components/FeatureListe.vue'
|
||||
import KontextMenue from './components/KontextMenue.vue'
|
||||
import PruefPanel from './components/PruefPanel.vue'
|
||||
import { offen as pruefungOffen, starte as startePruefung, schliesse as schliessePruefung } from './composables/pruefung.js'
|
||||
import {
|
||||
offen as pruefungOffen,
|
||||
laeuft as pruefungLaeuft,
|
||||
prozent as pruefungProzent,
|
||||
starte as startePruefung,
|
||||
schliesse as schliessePruefung,
|
||||
} from './composables/pruefung.js'
|
||||
|
||||
const projekte = ref([])
|
||||
const aktivesProjekt = ref(null)
|
||||
@@ -80,12 +86,10 @@ onMounted(async () => {
|
||||
@geaendert="ladeProjekte"
|
||||
@umbenannt="umbenannt"
|
||||
>
|
||||
<button
|
||||
v-if="aktivesProjekt"
|
||||
class="icon-knopf"
|
||||
title="Karten prüfen"
|
||||
@click="startePruefung(aktivesProjekt)"
|
||||
>⌾</button>
|
||||
<span v-if="aktivesProjekt" class="pruefen">
|
||||
<button class="icon-knopf" title="Karten prüfen" @click="startePruefung(aktivesProjekt)">⌾</button>
|
||||
<span v-if="pruefungLaeuft" class="fortschritt">{{ pruefungProzent }} %</span>
|
||||
</span>
|
||||
<button class="icon-knopf" :title="TITEL[modus]" @click="wechsleDarkMode">
|
||||
{{ ICONS[modus] }}
|
||||
</button>
|
||||
@@ -118,4 +122,15 @@ main {
|
||||
margin-top: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
/* Fortschritt sitzt unter dem Prüf-Icon */
|
||||
.pruefen {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.fortschritt {
|
||||
font-size: 0.65rem;
|
||||
color: var(--dezent);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { nextTick, ref, watch } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import { featureSpeichern } from '../api.js'
|
||||
import { oeffneMenue } from '../composables/kontextmenue.js'
|
||||
import { erledige } from '../composables/pruefung.js'
|
||||
import { benenneUm, erledige, vorschlagsArt, vorschlagsZeilen } from '../composables/pruefung.js'
|
||||
import ModalBox from './ModalBox.vue'
|
||||
import ProblemHinweis from './ProblemHinweis.vue'
|
||||
|
||||
@@ -154,9 +154,13 @@ function baueRoh() {
|
||||
|
||||
async function speichern() {
|
||||
meldung.value = ''
|
||||
const alt = props.feature.id
|
||||
try {
|
||||
const f = await featureSpeichern(props.projekt, props.feature.id, baueRoh())
|
||||
emit('aktualisiert', props.feature.id, f) // id kann sich durch Titeländerung ändern
|
||||
const f = await featureSpeichern(props.projekt, alt, baueRoh())
|
||||
// Ein neuer Titel benennt die Datei um; die offenen Probleme müssen mitwandern,
|
||||
// sonst findet problemeFuer() sie unter der neuen id nicht mehr.
|
||||
if (f.id !== alt) benenneUm(alt, 'feature', alt, f.id)
|
||||
emit('aktualisiert', alt, f)
|
||||
} catch (e) {
|
||||
meldung.value = e.message
|
||||
}
|
||||
@@ -187,27 +191,53 @@ function problemeAn(feld, funktion = '', original = null) {
|
||||
(original === null || p.original === original))
|
||||
}
|
||||
|
||||
// Vorschlag übernehmen: Titel und Ziel ganz ersetzen, bei Regeln und Testen die Zeile
|
||||
// Bleibt die alte Zeile im Vorschlag stehen, kommt nur etwas dazu — sie ist in Ordnung
|
||||
function fehlerAn(feld, funktion = '', original = null) {
|
||||
return problemeAn(feld, funktion, original).some(p => vorschlagsArt(p) !== 'ergaenzen')
|
||||
}
|
||||
|
||||
// Der Vorschlag ersetzt die gefundene Zeile durch seine eigenen — keine, eine oder
|
||||
// mehrere. Gesucht wird über den Text, nicht über den Index: bei mehreren Vorschlägen
|
||||
// hintereinander hat sich die Liste schon verschoben.
|
||||
function zeileAendern(text, p) {
|
||||
const zeilen = text.split('\n')
|
||||
const i = zeilen.findIndex(z => z.replace(/^[-*]\s*/, '').trim() === p.original)
|
||||
if (i < 0) return null
|
||||
zeilen.splice(i, 1, ...vorschlagsZeilen(p).map(z => `- ${z}`))
|
||||
return zeilen.join('\n')
|
||||
}
|
||||
|
||||
// Vorschlag übernehmen. Rückgabe false = nicht anwendbar, dann bleibt er stehen
|
||||
function anwenden(p) {
|
||||
const ziel = p.funktion ? s.value.funktionen.find(f => f.text === p.funktion) : s.value
|
||||
if (!ziel) return
|
||||
if (!ziel) return false
|
||||
if (p.feld === 'titel') {
|
||||
if (p.funktion) ziel.text = p.vorschlag
|
||||
else ziel.titel = p.vorschlag
|
||||
if (!p.vorschlag) return false
|
||||
if (p.funktion) {
|
||||
ziel.text = p.vorschlag
|
||||
// Die übrigen Probleme dieser Funktion zeigen noch auf den alten Titel
|
||||
benenneUm(props.feature.id, 'funktion', p.funktion, p.vorschlag)
|
||||
} else {
|
||||
ziel.titel = p.vorschlag
|
||||
}
|
||||
} else if (p.feld === 'ziel') {
|
||||
ziel.ziel = p.vorschlag
|
||||
ziel.ziel = p.vorschlag // leerer Vorschlag leert das Feld — der Katalog erlaubt das
|
||||
} else if (p.feld === 'badge') {
|
||||
if (p.vorschlag !== 'kern' && p.vorschlag !== 'optional') return false
|
||||
ziel.art = p.vorschlag
|
||||
} else if (p.feld === 'regeln' || p.feld === 'testen') {
|
||||
if (typeof ziel[p.feld] !== 'string') return false // z. B. testen auf Kartenebene
|
||||
const neu = zeileAendern(ziel[p.feld], p)
|
||||
if (neu === null) return false // Zeile schon weg oder umgeschrieben
|
||||
ziel[p.feld] = neu
|
||||
} else {
|
||||
const feld = p.feld === 'testen' ? 'testen' : 'regeln'
|
||||
ziel[feld] = ziel[feld]
|
||||
.split('\n')
|
||||
.map(z => (z.replace(/^[-*]\s*/, '').trim() === p.original ? `- ${p.vorschlag}` : z))
|
||||
.join('\n')
|
||||
return false // unbekanntes Feld — lieber nichts anfassen
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function annehmen(p) {
|
||||
anwenden(p)
|
||||
erledige(p)
|
||||
if (anwenden(p)) erledige(p)
|
||||
await speichern()
|
||||
}
|
||||
|
||||
@@ -216,7 +246,7 @@ function ablehnen(p) {
|
||||
}
|
||||
|
||||
async function alleAnnehmen() {
|
||||
props.probleme.forEach(p => { anwenden(p); erledige(p) })
|
||||
props.probleme.forEach(p => { if (anwenden(p)) erledige(p) })
|
||||
await speichern()
|
||||
}
|
||||
|
||||
@@ -247,11 +277,15 @@ function alleAblehnen() {
|
||||
<button
|
||||
v-if="gross"
|
||||
class="badge"
|
||||
:class="s.art === 'kern' ? 'blau' : 'gruen'"
|
||||
:class="[s.art === 'kern' ? 'blau' : 'gruen', { fehler: fehlerAn('badge') }]"
|
||||
title="Klicken zum Umschalten"
|
||||
@click="wechsleArt"
|
||||
>{{ s.art }}</button>
|
||||
<span v-else class="badge" :class="s.art === 'kern' ? 'blau' : 'gruen'">{{ s.art }}</span>
|
||||
<span
|
||||
v-else
|
||||
class="badge"
|
||||
:class="[s.art === 'kern' ? 'blau' : 'gruen', { fehler: fehlerAn('badge') }]"
|
||||
>{{ s.art }}</span>
|
||||
<button
|
||||
class="icon-knopf"
|
||||
:title="gross ? 'Kompaktmodus' : 'Vollmodus'"
|
||||
@@ -263,6 +297,7 @@ function alleAblehnen() {
|
||||
</span>
|
||||
</header>
|
||||
<ProblemHinweis :probleme="problemeAn('titel')" @annehmen="annehmen" @ablehnen="ablehnen" />
|
||||
<ProblemHinweis :probleme="problemeAn('badge')" @annehmen="annehmen" @ablehnen="ablehnen" />
|
||||
|
||||
<section :class="{ leer: !s.ziel && !istAktiv('ziel') }">
|
||||
<p class="gruppe">Ziel</p>
|
||||
@@ -292,7 +327,7 @@ function alleAblehnen() {
|
||||
/>
|
||||
<div v-else-if="s.regeln" class="klickbar" @click="starte('regeln')">
|
||||
<template v-for="(r, k) in punkte(s.regeln)" :key="k">
|
||||
<div class="regel" :class="{ fehler: problemeAn('regeln', '', r).length }">{{ r }}</div>
|
||||
<div class="regel" :class="{ fehler: fehlerAn('regeln', '', r) }">{{ r }}</div>
|
||||
<ProblemHinweis
|
||||
:probleme="problemeAn('regeln', '', r)"
|
||||
@annehmen="annehmen"
|
||||
@@ -326,7 +361,7 @@ function alleAblehnen() {
|
||||
<div
|
||||
v-else
|
||||
class="funktionstitel klickbar"
|
||||
:class="{ fehler: problemeAn('titel', f.text).length }"
|
||||
:class="{ fehler: fehlerAn('titel', f.text) }"
|
||||
@click="starte('funktion', i)"
|
||||
>{{ f.text }}</div>
|
||||
</div>
|
||||
@@ -363,7 +398,7 @@ function alleAblehnen() {
|
||||
/>
|
||||
<div v-else class="klickbar" @click="starte('regeln', i)">
|
||||
<template v-for="(r, k) in punkte(f.regeln)" :key="k">
|
||||
<div class="regel" :class="{ fehler: problemeAn('regeln', f.text, r).length }">{{ r }}</div>
|
||||
<div class="regel" :class="{ fehler: fehlerAn('regeln', f.text, r) }">{{ r }}</div>
|
||||
<ProblemHinweis
|
||||
:probleme="problemeAn('regeln', f.text, r)"
|
||||
@annehmen="annehmen"
|
||||
@@ -387,7 +422,7 @@ function alleAblehnen() {
|
||||
/>
|
||||
<div v-else class="klickbar" @click="starte('testen', i)">
|
||||
<template v-for="(schritt, k) in punkte(f.testen)" :key="k">
|
||||
<div class="schritt" :class="{ fehler: problemeAn('testen', f.text, schritt).length }">
|
||||
<div class="schritt" :class="{ fehler: fehlerAn('testen', f.text, schritt) }">
|
||||
<span class="nr">{{ k + 1 }}.</span>{{ schritt }}
|
||||
</div>
|
||||
<ProblemHinweis
|
||||
@@ -431,6 +466,7 @@ function alleAblehnen() {
|
||||
/* Prüfung: fehlerhafte Karte und Stelle rot */
|
||||
.karte.fehlerhaft { border-color: var(--rot); }
|
||||
.fehler { color: var(--rot); }
|
||||
.badge.fehler { color: var(--rot); background: var(--rot-bg); }
|
||||
.alle { white-space: nowrap; }
|
||||
|
||||
/* Vollmodus: volle Breite, Funktionen als Boxen in responsiven Spalten,
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
<script setup>
|
||||
import { nextTick, ref, watch } from 'vue'
|
||||
import { kontextMenue, schliesseMenue } from '../composables/kontextmenue.js'
|
||||
|
||||
const ABSTAND = 4
|
||||
|
||||
const menue = ref(null)
|
||||
const pos = ref({ left: '0px', top: '0px' })
|
||||
|
||||
// Erst an der Mausposition zeichnen, dann anhand der gemessenen Größe ins Fenster rücken
|
||||
watch(kontextMenue, async (m) => {
|
||||
if (!m) return
|
||||
pos.value = { left: `${m.x}px`, top: `${m.y}px` }
|
||||
await nextTick()
|
||||
const el = menue.value
|
||||
if (!el) return
|
||||
const { width, height } = el.getBoundingClientRect()
|
||||
const x = Math.min(m.x, window.innerWidth - width - ABSTAND)
|
||||
const y = Math.min(m.y, window.innerHeight - height - ABSTAND)
|
||||
pos.value = { left: `${Math.max(ABSTAND, x)}px`, top: `${Math.max(ABSTAND, y)}px` }
|
||||
})
|
||||
|
||||
function waehle(eintrag) {
|
||||
schliesseMenue()
|
||||
eintrag.aktion()
|
||||
@@ -8,11 +27,7 @@ function waehle(eintrag) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="kontextMenue"
|
||||
class="kontextmenue"
|
||||
:style="{ left: kontextMenue.x + 'px', top: kontextMenue.y + 'px' }"
|
||||
>
|
||||
<div v-if="kontextMenue" ref="menue" class="kontextmenue" :style="pos">
|
||||
<button v-for="e in kontextMenue.eintraege" :key="e.text" @click="waehle(e)">{{ e.text }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
titel: { type: String, required: true },
|
||||
@@ -18,7 +18,16 @@ function bestaetigen() {
|
||||
emit('bestaetigt', wert.value.trim())
|
||||
}
|
||||
|
||||
onMounted(() => feld.value?.focus())
|
||||
// Am Fenster statt am Eingabefeld: die Rückfrage hat gar keins
|
||||
function beiTaste(e) {
|
||||
if (e.key === 'Escape') emit('abbrechen')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
feld.value?.focus()
|
||||
window.addEventListener('keydown', beiTaste)
|
||||
})
|
||||
onUnmounted(() => window.removeEventListener('keydown', beiTaste))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -32,7 +41,6 @@ onMounted(() => feld.value?.focus())
|
||||
v-model="wert"
|
||||
:placeholder="platzhalter"
|
||||
@keyup.enter="bestaetigen"
|
||||
@keydown.esc="emit('abbrechen')"
|
||||
>
|
||||
<p v-if="meldung" class="meldung">{{ meldung }}</p>
|
||||
<footer>
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<script setup>
|
||||
import { vorschlagsArt } from '../composables/pruefung.js'
|
||||
|
||||
defineProps({
|
||||
probleme: { type: Array, required: true },
|
||||
})
|
||||
const emit = defineEmits(['annehmen', 'ablehnen'])
|
||||
|
||||
// Was der Vorschlag bewirkt, steht nicht in der Antwort — es ergibt sich aus dem
|
||||
// Vergleich mit der alten Zeile. Bei entfernen steht dort die Zeile, die weggeht.
|
||||
const ZEICHEN = { ergaenzen: '+', entfernen: '−' }
|
||||
const zeichen = (p) => ZEICHEN[vorschlagsArt(p)] || ''
|
||||
const text = (p) => (vorschlagsArt(p) === 'entfernen' ? p.original : p.vorschlag)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -14,7 +22,9 @@ const emit = defineEmits(['annehmen', 'ablehnen'])
|
||||
<button class="icon-knopf rot" title="Ablehnen" @click="emit('ablehnen', p)">✕</button>
|
||||
</span>
|
||||
</div>
|
||||
<p class="vorschlag">{{ p.vorschlag }}</p>
|
||||
<p class="vorschlag" :class="vorschlagsArt(p)">
|
||||
<span v-if="zeichen(p)" class="zeichen">{{ zeichen(p) }}</span>{{ text(p) }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -48,5 +58,13 @@ const emit = defineEmits(['annehmen', 'ablehnen'])
|
||||
margin: 0.1rem 0 0;
|
||||
font-size: 0.85rem;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-line; /* mehrzeilige Vorschläge bleiben mehrzeilig */
|
||||
}
|
||||
.vorschlag.entfernen { text-decoration: line-through; color: var(--dezent); }
|
||||
.zeichen {
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,8 +11,8 @@ import { laeuft, prozent, fertig, gesamt, meldung, probleme, schliesse } from '.
|
||||
|
||||
<p class="fortschritt">
|
||||
<template v-if="laeuft">{{ prozent }} % geprüft</template>
|
||||
<template v-else-if="gesamt">{{ gesamt }} Karten geprüft</template>
|
||||
<template v-else>Keine Karten</template>
|
||||
<template v-else-if="gesamt">{{ gesamt }} Stellen geprüft</template>
|
||||
<template v-else>Nichts zu prüfen</template>
|
||||
<span v-if="laeuft" class="dezent"> ({{ fertig }}/{{ gesamt }})</span>
|
||||
</p>
|
||||
|
||||
@@ -22,8 +22,9 @@ import { laeuft, prozent, fertig, gesamt, meldung, probleme, schliesse } from '.
|
||||
|
||||
<ul>
|
||||
<li v-for="(p, i) in probleme" :key="i">
|
||||
<span class="stelle">{{ p.funktion || p.feature }} · {{ p.feld }}</span>
|
||||
<span class="stelle">{{ p.feature }}<template v-if="p.funktion"> · {{ p.funktion }}</template> · {{ p.feld }}</span>
|
||||
<span class="problem">{{ p.problem }}</span>
|
||||
<span class="original">{{ p.original }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
@@ -69,4 +70,9 @@ li {
|
||||
color: var(--akzent);
|
||||
}
|
||||
.problem { font-size: 0.85rem; }
|
||||
.original {
|
||||
font-size: 0.78rem;
|
||||
color: var(--dezent);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,7 @@ export const gesamt = ref(0)
|
||||
export const meldung = ref('')
|
||||
export const probleme = ref([])
|
||||
|
||||
const erledigt = new Set() // angenommene oder abgelehnte Probleme kommen nicht zurück
|
||||
const gesehen = new Set() // schon übernommene Probleme holt der nächste Abruf nicht erneut
|
||||
let projektName = null
|
||||
let uhr = null
|
||||
|
||||
@@ -25,12 +25,41 @@ export function problemeFuer(featureId) {
|
||||
return probleme.value.filter(p => p.feature === featureId)
|
||||
}
|
||||
|
||||
// Der Vorschlag ersetzt die Stelle eins zu eins. Was das bedeutet, steht nicht in der
|
||||
// Antwort — es ergibt sich aus dem Vergleich. So muss das Modell keine Aktion wählen.
|
||||
export function vorschlagsZeilen(p) {
|
||||
return (p.vorschlag || '').split('\n').map(z => z.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
export function vorschlagsArt(p) {
|
||||
const zeilen = vorschlagsZeilen(p)
|
||||
if (!zeilen.length) return 'entfernen'
|
||||
// Steht die alte Zeile wieder vorn, kommt nur etwas dazu
|
||||
if (zeilen.length > 1 && zeilen[0] === p.original) return 'ergaenzen'
|
||||
return 'anpassen'
|
||||
}
|
||||
|
||||
// Neue Probleme werden angehängt, nicht ersetzt. Der Server kennt nur die Namen von
|
||||
// vor dem Lauf; würde die Liste jede Sekunde neu gesetzt, verlöre benenneUm() seine Wirkung.
|
||||
function uebernehmen(stand) {
|
||||
laeuft.value = stand.laeuft
|
||||
fertig.value = stand.fertig
|
||||
gesamt.value = stand.gesamt
|
||||
meldung.value = stand.meldung
|
||||
probleme.value = stand.probleme.filter(p => !erledigt.has(schluessel(p)))
|
||||
for (const p of stand.probleme) {
|
||||
const k = schluessel(p)
|
||||
if (gesehen.has(k)) continue
|
||||
gesehen.add(k)
|
||||
probleme.value.push(p)
|
||||
}
|
||||
}
|
||||
|
||||
// Ein angenommener Titelvorschlag benennt Karte oder Funktion um. Die übrigen Probleme
|
||||
// zeigen dann noch auf den alten Namen und wären in der Karte nicht mehr auffindbar.
|
||||
export function benenneUm(featureId, feld, alt, neu) {
|
||||
probleme.value.forEach(p => {
|
||||
if (p.feature === featureId && p[feld] === alt) p[feld] = neu
|
||||
})
|
||||
}
|
||||
|
||||
async function abfragen() {
|
||||
@@ -52,7 +81,7 @@ export async function starte(projekt) {
|
||||
projektName = projekt
|
||||
offen.value = true
|
||||
meldung.value = ''
|
||||
erledigt.clear()
|
||||
gesehen.clear()
|
||||
probleme.value = []
|
||||
try {
|
||||
uebernehmen(await pruefungStarten(projekt))
|
||||
@@ -74,10 +103,10 @@ export async function starte(projekt) {
|
||||
uhr = setInterval(abfragen, 1000)
|
||||
}
|
||||
|
||||
// Angenommenes oder abgelehntes Problem verschwindet, die übrigen bleiben stehen
|
||||
// Angenommenes oder abgelehntes Problem verschwindet, die übrigen bleiben stehen.
|
||||
// Vergleich über die Identität: der Schlüssel kann sich durch benenneUm() ändern.
|
||||
export function erledige(problem) {
|
||||
erledigt.add(schluessel(problem))
|
||||
probleme.value = probleme.value.filter(p => schluessel(p) !== schluessel(problem))
|
||||
probleme.value = probleme.value.filter(p => p !== problem)
|
||||
}
|
||||
|
||||
export async function schliesse() {
|
||||
|
||||
Reference in New Issue
Block a user