Erweitert die Makro-Sprache um Netzwerkzugriff: "x = GET(url)"/ "x = POST(url; body)" binden die geparste JSON-Antwort an eine Variable; FOR item IN x DO ... END iteriert, wenn x ein JSON-Array ist, über dessen Elemente; JSONGET(wert; pfad) liest ein Feld heraus (punktgetrennt, Zahl = Array-Index) und funktioniert – anders als GET/POST – überall in einem Ausdruck verschachtelt, nicht nur als ganze Zuweisung. Da ein echter Netzwerkaufruf nicht synchron laufen kann, ohne den Tab einzufrieren, bekommt der Makro-Interpreter einen separaten asynchronen Zweig (execBlockAsync/execStatementAsync); der bestehende synchrone Interpreter, den auch gewöhnliche "Mit Schleife"-Funktionen mitten in der synchronen Neuberechnung nutzen, bleibt unangetastet. Netzwerkaufrufe werden pro Makro einmalig mit Zielname bestätigt (dauerhaft gemerkt); die bestehende "ein Lauf = ein Undo-Schritt"-Garantie, die bislang auf Synchronität beruhte, wird jetzt explizit wiederhergestellt statt sich weiter auf die Zeit-Koaleszenz zu verlassen. Zwei echte Lücken erst beim eigenen Live-Test gefunden und behoben: (1) JSONGET direkt in eine Aktion verschachtelt (statt erst einer Variable zugewiesen) scheiterte zunächst an #NAME?; (2) JSONGET(...) direkt als FOR-Quelle (ohne Zwischenvariable) scheiterte sowohl an der Speicher-Zeit-Prüfung als auch zur Laufzeit, da substitute() einen Funktionsaufruf nie auswertet. Live gegen eine neue same-origin-Fixture (public/test-fixtures/sample.json) verifiziert: echter GET, echter Bestätigungsdialog, FOR/JSONGET über die echte Antwort, kein erneuter Dialog beim zweiten Lauf. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
130 lines
3.5 KiB
Vue
130 lines
3.5 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* A clickable canvas button object -- one of the two ways to trigger a
|
|
* macro (the other is the "▶ Ausführen" row in MacrosModal.vue). Caption
|
|
* (`label`) and colours are edited in the inspector (SettingsPanel.vue);
|
|
* which macro it runs is assignable both there and from this component's
|
|
* own right-click menu, for a quicker "build a button, wire it up, done"
|
|
* loop without leaving the canvas.
|
|
*/
|
|
import { computed, reactive, ref } from 'vue';
|
|
import { useDocument } from '../store/document';
|
|
import { useToasts } from '../composables/useToasts';
|
|
import ContextMenu from './ContextMenu.vue';
|
|
|
|
const props = defineProps<{ objId: string }>();
|
|
const store = useDocument();
|
|
const toasts = useToasts();
|
|
|
|
const obj = computed(() => {
|
|
const o = store.objectById(props.objId);
|
|
return o && o.kind === 'button' ? o : undefined;
|
|
});
|
|
|
|
// A macro can now involve a real network round-trip (GET/POST) and even a
|
|
// blocking confirm() prompt -- guard against a second click starting a
|
|
// second overlapping run while one is still in flight.
|
|
const running = ref(false);
|
|
|
|
async function run() {
|
|
const o = obj.value;
|
|
if (!o || running.value) return;
|
|
if (!o.macroId || !store.doc.macros.some((m) => m.id === o.macroId)) {
|
|
toasts.info('Diesem Button ist noch kein Makro zugewiesen — per Rechtsklick oder im Inspektor.');
|
|
return;
|
|
}
|
|
running.value = true;
|
|
try {
|
|
const result = await store.runMacro(o.macroId);
|
|
if (result.ok) toasts.success(result.message);
|
|
else toasts.error(result.message);
|
|
} finally {
|
|
running.value = false;
|
|
}
|
|
}
|
|
|
|
const boxStyle = computed(() => {
|
|
const o = obj.value;
|
|
if (!o) return {};
|
|
return {
|
|
color: o.fg || 'var(--accent)',
|
|
background: o.bg || 'var(--accent-weak)',
|
|
} as Record<string, string>;
|
|
});
|
|
|
|
// ---- right-click: run / (re)assign a macro without opening the inspector ----
|
|
const menu = reactive({ open: false, x: 0, y: 0 });
|
|
function onContextMenu(e: MouseEvent) {
|
|
e.preventDefault();
|
|
menu.x = e.clientX;
|
|
menu.y = e.clientY;
|
|
menu.open = true;
|
|
}
|
|
const menuItems = computed(() => {
|
|
const o = obj.value;
|
|
if (!o) return [];
|
|
const items: { label: string; action: () => void }[] = [];
|
|
if (o.macroId) {
|
|
items.push({ label: '▶ Makro ausführen', action: run });
|
|
}
|
|
for (const m of store.doc.macros) {
|
|
items.push({
|
|
label: `${m.id === o.macroId ? '✓ ' : ''}Makro: ${m.name}`,
|
|
action: () => store.setButtonMacroId(o.id, m.id),
|
|
});
|
|
}
|
|
if (store.doc.macros.length === 0) {
|
|
items.push({ label: 'Kein Makro vorhanden — erst in „▶ Makros" anlegen', action: () => {} });
|
|
}
|
|
if (o.macroId) {
|
|
items.push({ label: 'Zuweisung entfernen', action: () => store.setButtonMacroId(o.id, null) });
|
|
}
|
|
return items;
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
v-if="obj"
|
|
class="btnbox"
|
|
:class="{ running }"
|
|
:style="boxStyle"
|
|
@click="run"
|
|
@contextmenu="onContextMenu"
|
|
>
|
|
<span class="label">{{ running ? '…' : obj.label || 'Button' }}</span>
|
|
<ContextMenu v-model:open="menu.open" :x="menu.x" :y="menu.y" :items="menuItems" />
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.btnbox {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 8px 14px;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
.btnbox.running {
|
|
opacity: 0.6;
|
|
cursor: default;
|
|
}
|
|
.btnbox:hover {
|
|
filter: brightness(1.06);
|
|
}
|
|
.btnbox:active {
|
|
filter: brightness(0.96);
|
|
}
|
|
.label {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
</style>
|