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:
@@ -1524,6 +1524,22 @@ describe('macros (document-level, explicitly-triggered write scripts)', () => {
|
||||
expect(s.tablesOf(sheetId).length).toBe(before); // added then deleted -- net zero
|
||||
});
|
||||
|
||||
it('runMacro: ADDTABLE on a DIFFERENT sheet does not yank the active sheet/selection there (found live: addTable() itself navigates, a macro must undo just that incidental side effect)', () => {
|
||||
const s = fresh();
|
||||
const startSheetId = s.doc.activeSheetId;
|
||||
const startSelection = s.doc.selection;
|
||||
const otherSheetId = s.addSheet('Ziel');
|
||||
s.setActiveSheet(startSheetId); // addSheet() itself also navigates -- go back
|
||||
expect(s.doc.activeSheetId).toBe(startSheetId);
|
||||
|
||||
const macroId = s.addMacro('M', 'ADDTABLE("Ziel"; "Neu")') as string;
|
||||
expect(s.runMacro(macroId).ok).toBe(true);
|
||||
|
||||
expect(s.doc.activeSheetId).toBe(startSheetId); // NOT switched to "Ziel"
|
||||
expect(s.doc.selection).toEqual(startSelection);
|
||||
expect(s.tablesOf(otherSheetId).map((t) => t.name)).toContain('Neu'); // the mutation itself still happened
|
||||
});
|
||||
|
||||
it('runMacro: ADDSHEET/RENAMESHEET/DELETESHEET round-trip', () => {
|
||||
const s = fresh();
|
||||
const before = s.doc.sheets.length;
|
||||
|
||||
+71
-3
@@ -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,
|
||||
|
||||
+26
-1
@@ -122,7 +122,32 @@ export interface TextObject {
|
||||
bg?: string;
|
||||
}
|
||||
|
||||
export type SheetObject = TableObject | ChartObject | TextObject;
|
||||
/** A clickable canvas object that runs an assigned macro (see `Macro`
|
||||
* below) when clicked -- the second of the two trigger UIs the macro
|
||||
* feature offers, alongside the "▶ Ausführen" button in the Makros panel.
|
||||
* Freely positioned/resized like a chart or text box, no intrinsic size. */
|
||||
export interface ButtonObject {
|
||||
kind: 'button';
|
||||
id: string;
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
/** button caption, independent of `name` (the canvas-object label shown
|
||||
* in its chrome/rename-on-dblclick) */
|
||||
label: string;
|
||||
/** which macro a click runs; `null` = unassigned (shows a hint instead
|
||||
* of running anything). A dangling id (its macro was deleted, or an
|
||||
* imported document's macro wasn't carried along) is treated the same
|
||||
* as `null` -- the same lenient convention as a chart's dangling
|
||||
* `sourceTableId`. */
|
||||
macroId: string | null;
|
||||
bg?: string;
|
||||
fg?: string;
|
||||
}
|
||||
|
||||
export type SheetObject = TableObject | ChartObject | TextObject | ButtonObject;
|
||||
|
||||
export interface Sheet {
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user