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>
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
<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("Umsatz"; 1) SETVALUE(Umsatz::A2; "Neu")"
|
||||
/>
|
||||
</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>
|
||||
Reference in New Issue
Block a user