Files
numolus/src/components/MacrosModal.vue
T
wkhandClaude Sonnet 5 a59fda25b0 Makros: UI (Button-Objekt, Verwaltungspanel, Symbolleiste)
Zweiter von zwei Commits (Fundament in e670f13). Neues, frei platzierbares
Button-Objekt auf der Fläche, das per Klick ein zugewiesenes Makro
ausführt (Zuweisung im Inspektor oder per Rechtsklick direkt am Button).
Neues "▶ Makros"-Panel (Liste + Formular, dritter "▶ Ausführen"-Button pro
Zeile) für Anlegen/Bearbeiten/Löschen/Testlauf ohne Canvas-Button.

Beim Live-Testen gefunden und behoben: ADDTABLE/ADDSHEET navigieren als
Nebeneffekt zur eingefügten Tabelle/zum eingefügten Blatt (richtig für
einen Symbolleisten-Klick, überraschend für einen Makro-Lauf im
Hintergrund) — runMacro() sichert und stellt jetzt aktives Blatt/Objekt/
Auswahl nach dem Lauf wieder her. Nebenbei zwei bestehende
"impliziter-Else"-Fehler behoben, die mit einer vierten Objektart zu
echten Fehlerquellen geworden wären (io/json.ts normaliseObject,
CanvasObject.vue Render-Dispatch).

Vollständig live per Playwright End-zu-Ende verifiziert: Makro über das
echte Formular angelegt, per Listen-Button und per Button-Objekt
ausgeführt, Rechtsklick-Zuweisung geprüft, ein absichtlich fehlerhaftes
Makro zeigt einen Fehler-Toast statt abzustürzen — keine Konsolenfehler.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 10:07:56 +02:00

268 lines
7.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/**
* "▶ Makros" -- the exclusive UI for creating, viewing, editing, deleting
* and test-running the document's macros (store/document.ts's runMacro(),
* engine/parser.ts's parseMacroBlock()). Mirrors CustomFunctionsModal.vue's
* list/edit-form shape, minus the parameter list and expression/block
* toggle a macro doesn't have -- a macro is always the same write-capable
* statement language, parameterless (triggered by a click, no arguments).
*/
import { reactive, ref, watch } from 'vue';
import { useDocument } from '../store/document';
import { useToasts } from '../composables/useToasts';
import BaseModal from './BaseModal.vue';
const props = defineProps<{ open: boolean }>();
const emit = defineEmits<{ (e: 'close'): void }>();
const store = useDocument();
const toasts = useToasts();
type Mode = 'list' | 'edit';
const mode = ref<Mode>('list');
const editingId = ref<string | null>(null);
const form = reactive({ name: '', body: '' });
const formError = ref<string | null>(null);
watch(
() => props.open,
(o) => {
if (o) {
mode.value = 'list';
formError.value = null;
}
},
);
function openNew() {
editingId.value = null;
form.name = '';
form.body = '';
formError.value = null;
mode.value = 'edit';
}
function openEdit(id: string) {
const m = store.doc.macros.find((x) => x.id === id);
if (!m) return;
editingId.value = id;
form.name = m.name;
form.body = m.body;
formError.value = null;
mode.value = 'edit';
}
function save() {
const result = editingId.value
? store.updateMacro(editingId.value, form.name, form.body)
: store.addMacro(form.name, form.body);
if (result && typeof result === 'object' && 'error' in result) {
formError.value = result.error;
return;
}
formError.value = null;
mode.value = 'list';
}
function cancelEdit() {
formError.value = null;
mode.value = 'list';
}
function removeMacro(id: string, name: string) {
if (confirm(`Makro "${name}" löschen? Buttons, die es zugewiesen haben, verlieren die Zuweisung.`)) {
store.deleteMacro(id);
}
}
function runMacro(id: string) {
const result = store.runMacro(id);
if (result.ok) toasts.success(result.message);
else toasts.error(result.message);
}
function close() {
emit('close');
}
</script>
<template>
<BaseModal
:open="open"
:title="mode === 'list' ? 'Makros' : editingId ? 'Makro bearbeiten' : 'Neues Makro'"
@close="close"
>
<!-- ---- list view ---- -->
<template v-if="mode === 'list'">
<p v-if="!store.doc.macros.length" class="hint">
Noch keine Makros definiert. Mit „+ Neues Makro" eines anlegen — danach über „▶" hier
oder über einen Button auf der Fläche auslösbar.
</p>
<ul v-else class="fn-list">
<li v-for="m in store.doc.macros" :key="m.id" class="fn-row">
<code class="fn-sig">{{ m.name }}</code>
<span class="fn-actions">
<button class="mini run" title="Ausführen" @click="runMacro(m.id)">▶ Ausführen</button>
<button class="mini" @click="openEdit(m.id)">Bearb.</button>
<button class="mini danger" title="Löschen" @click="removeMacro(m.id, m.name)">🗑</button>
</span>
</li>
</ul>
</template>
<!-- ---- edit / create form ---- -->
<template v-else>
<label class="row">
Name
<input v-model="form.name" type="text" placeholder="z. B. Quartalsabschluss" />
</label>
<label class="row body-row">
Aktionen
<textarea
v-model="form.body"
class="formula-input"
rows="7"
placeholder="INSERTROW(&quot;Umsatz&quot;; 1)&#10;SETVALUE(Umsatz::A2; &quot;Neu&quot;)"
/>
</label>
<p class="hint">
Dieselbe Sprache wie „Mit Schleife"-Funktionen (<strong>Zuweisungen</strong>,
<strong>FOR</strong>/<strong>IF/ELSE</strong>, <code>RETURN</code> als vorzeitiger
Abbruch — ein <code>RETURN "text"</code> wird die Erfolgsmeldung), plus elf
<strong>Schreib-Aktionen</strong>:
<code>SETVALUE(Ziel; Wert)</code>,
<code>INSERTROW</code>/<code>DELETEROW</code>/<code>INSERTCOL</code>/<code>DELETECOL(Tabelle; Index)</code>,
<code>ADDTABLE(Blatt; Name)</code>, <code>RENAMETABLE</code>/<code>DELETETABLE(Tabelle)</code>,
<code>ADDSHEET(Name)</code>, <code>RENAMESHEET</code>/<code>DELETESHEET(Blatt)</code>.
Tabellen/Blätter werden als Text benannt (<code>"Umsatz"</code> oder
<code>"Blatt2!Kosten"</code>). <strong>Jeder Zell-/Bereichsbezug muss die Tabelle
nennen</strong> — <code>Umsatz::A1</code>, nie bloß <code>A1</code> — ein Makro hat
anders als eine Zelle keine eigene Tabelle. Beispiel:
<code>SETVALUE(Umsatz::B2; Umsatz::B2 + 1) INSERTROW("Umsatz"; 0)</code>.
</p>
<p v-if="formError" class="form-error">⚠ {{ formError }}</p>
</template>
<template #footer>
<template v-if="mode === 'list'">
<button class="primary" @click="openNew">+ Neues Makro</button>
</template>
<template v-else>
<button @click="cancelEdit">Abbrechen</button>
<button class="primary" @click="save">Speichern</button>
</template>
</template>
</BaseModal>
</template>
<style scoped>
.hint {
color: var(--text-dim);
font-size: 12.5px;
line-height: 1.5;
margin: 0;
}
.hint code {
background: var(--surface-2);
border-radius: 4px;
padding: 1px 5px;
}
.fn-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.fn-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 6px 8px;
border-radius: 6px;
background: var(--surface-2);
}
.fn-sig {
font-size: 12.5px;
font-family: inherit;
overflow-wrap: anywhere;
}
.fn-actions {
display: flex;
gap: 6px;
flex: none;
}
.mini {
border: 1px solid var(--border);
background: var(--surface);
border-radius: 6px;
padding: 3px 9px;
font-size: 12px;
cursor: pointer;
}
.mini:hover {
border-color: var(--accent);
color: var(--accent);
}
.mini.run {
border-color: var(--accent);
color: var(--accent);
font-weight: 600;
}
.mini.run:hover {
background: var(--accent);
color: #fff;
}
.mini.danger:hover {
border-color: #dc2626;
color: #dc2626;
}
.row {
display: flex;
flex-direction: column;
gap: 5px;
margin-bottom: 12px;
font-size: 12.5px;
color: var(--text-dim);
}
.row input[type='text'] {
border: 1px solid var(--border);
border-radius: 6px;
padding: 5px 8px;
font: inherit;
background: var(--surface-2);
color: var(--text);
}
.body-row .formula-input {
min-height: 30px;
max-height: 40vh;
}
.formula-input {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 6px 10px;
font: inherit;
font-variant-numeric: tabular-nums;
color: var(--text);
background: var(--surface-2);
resize: vertical;
white-space: pre-wrap;
word-break: break-word;
}
.formula-input:focus {
outline: none;
border-color: var(--accent);
background: var(--surface);
}
.form-error {
margin: 4px 0 0;
font-size: 12.5px;
color: #dc2626;
line-height: 1.4;
}
</style>