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:
wkh
2026-09-09 10:07:56 +02:00
co-authored by Claude Sonnet 5
parent e670f13077
commit a59fda25b0
11 changed files with 759 additions and 30 deletions
+71 -3
View File
@@ -58,6 +58,7 @@ import {
type NumberLocale,
} from './format';
import type {
ButtonObject,
CellCoord,
CellFormat,
CellGraphNode,
@@ -862,6 +863,44 @@ function createStore() {
o.height = Math.max(32, Math.round(height));
}
// ---- buttons (canvas objects that run a macro on click) ---------------
function addButton(sheetId: string, pos?: { x: number; y: number }): string {
const sheet = sheetById(sheetId);
if (!sheet) return '';
const id = uid('btn');
const p = pos ?? nextCascade(sheet);
const button: ButtonObject = {
kind: 'button',
id,
name: uniqueName('Button', objectNames()),
x: Math.max(0, Math.round(p.x)),
y: Math.max(0, Math.round(p.y)),
width: 140,
height: 44,
label: 'Button',
macroId: null,
};
sheet.objects.push(button);
doc.activeObjectId = id;
doc.activeSheetId = sheetId;
bump();
return id;
}
function setButtonLabel(objId: string, label: string) {
const o = objectById(objId);
if (o?.kind === 'button') o.label = label;
}
function setButtonMacroId(objId: string, macroId: string | null) {
const o = objectById(objId);
if (o?.kind === 'button') o.macroId = macroId;
}
function resizeButton(objId: string, width: number, height: number) {
const o = objectById(objId);
if (o?.kind !== 'button') return;
o.width = Math.max(60, Math.round(width));
o.height = Math.max(28, Math.round(height));
}
function renameObject(objId: string, name: string) {
const o = objectById(objId);
const clean = name.trim();
@@ -1318,6 +1357,24 @@ function createStore() {
const macro = doc.macros.find((m) => m.id === macroId);
if (!macro) return { ok: false, message: 'Makro nicht gefunden.' };
// addTable/addSheet/... are UI-oriented primitives that also navigate
// the user to what they just inserted -- exactly right for a toolbar
// click, but a surprising side effect for a macro run in the
// background (e.g. a button click on "Analyse" silently switching the
// view to some other sheet an ADDTABLE deep in the script happened to
// touch). Found live: running a macro containing ADDTABLE("Blatt 2";
// ...) yanked the active sheet to "Blatt 2". Snapshotting + restoring
// the view state (unless the macro deleted it out from under us) keeps
// the mutation but undoes only its incidental navigation.
const savedSheetId = doc.activeSheetId;
const savedObjectId = doc.activeObjectId;
const savedSelection = doc.selection ? { ...doc.selection } : null;
const restoreView = () => {
if (sheetById(savedSheetId)) doc.activeSheetId = savedSheetId;
doc.activeObjectId = savedObjectId && objectById(savedObjectId) ? savedObjectId : null;
doc.selection = savedSelection && objectById(savedSelection.tableId) ? savedSelection : null;
};
const macroHost: EngineHost = {
// Never actually consulted: validateMacro rejects every unqualified
// cell/range reference at save time, so evalScalar's 'cell' case
@@ -1386,6 +1443,8 @@ function createStore() {
return { ok: true, message: typeof result === 'string' ? result : 'Makro ausgeführt.' };
} catch (e) {
return { ok: false, message: e instanceof Error ? e.message : String(e) };
} finally {
restoreView();
}
}
@@ -1446,10 +1505,15 @@ function createStore() {
}
// ---- number-format locale (object -> sheet -> document cascade) ---
/** Effective locale for a canvas object (table or chart). */
/** Effective locale for a canvas object (table or chart) -- text/button
* objects have no `locale` field of their own (nothing they show is
* locale-formatted), so they fall straight through to the sheet/document
* cascade. */
function effectiveLocale(objId: string | null): NumberLocale {
const o = objId ? objectById(objId) : undefined;
if (o && o.kind !== 'text' && o.locale && isNumberLocale(o.locale)) return o.locale;
if (o && (o.kind === 'table' || o.kind === 'chart') && o.locale && isNumberLocale(o.locale)) {
return o.locale;
}
const sheetId = objId ? sheetIdOfObject(objId) : doc.activeSheetId;
const sheetLoc = sheetId ? sheetById(sheetId)?.locale : undefined;
if (sheetLoc && isNumberLocale(sheetLoc)) return sheetLoc;
@@ -1468,7 +1532,7 @@ function createStore() {
}
function setObjectLocale(objId: string, l: string | null) {
const o = objectById(objId);
if (!o || o.kind === 'text') return;
if (!o || (o.kind !== 'table' && o.kind !== 'chart')) return;
if (l && isNumberLocale(l)) o.locale = l;
else delete o.locale;
}
@@ -2419,6 +2483,10 @@ function createStore() {
setTextContent,
setTextFormat,
resizeText,
addButton,
setButtonLabel,
setButtonMacroId,
resizeButton,
renameObject,
deleteObject,
moveObject,