Interaktives Abhängigkeiten-Panel (Vorgänger/Nachfolger, andockbar)
Rechtsklick auf eine Zelle → "🕸 Abhängigkeiten anzeigen" öffnet ein an der rechten Seite angedocktes Panel (wie das Einstellungen-Panel links) mit den direkten Vorgängern (was die Zelle liest) und Nachfolgern (was sie liest), auch über Tabellen-/Blattgrenzen hinweg. Ein Klick auf einen Eintrag navigiert wirklich dorthin: wählt die echte Zelle im Dokument aus, wechselt bei Bedarf das Blatt-Tab, und das Panel zentriert sich neu — Blatt-Tabs, Canvas, Formel- und Statusleiste bleiben dabei die ganze Zeit sichtbar und bedienbar (kein Vollbild-Dialog). Jeder Eintrag zeigt immer den Blattnamen, nie nur implizit. Zirkelbezüge werden rot markiert. Neu in der Engine: Workbook.graphPrecedents/graphDependents, dünne öffentliche Wrapper um den bereits vorhandenen DependencyGraph. Im Store: cellGraphSelf/cellGraphNeighbors (Anreicherung für die Anzeige) sowie openDependencyGraph/closeDependencyGraph (dokumentweiter UI-State, ruft intern das bestehende selectCell auf). 424 Tests (Engine-/Store-Ebene vollständig abgedeckt), vue-tsc/eslint/build sauber. Das Panel selbst nur live per Playwright verifiziert (kein Komponenten-Test-Setup vorhanden, in README dokumentiert). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -558,6 +558,16 @@ gefunden wird — dieselbe konservative Konvention wie bei `IF`.
|
||||
Strukturänderungen (Tabelle umbenennen/löschen, Zeilen/Spalten, Blatt löschen)
|
||||
rufen `rebuild()` für einen vollständigen – trotzdem billigen – Durchlauf.
|
||||
|
||||
**`Workbook.graphPrecedents`/`graphDependents`** (dünne öffentliche Wrapper
|
||||
um `DependencyGraph.getPrecedents`/`getDependents`) sind der einzige Ort, an
|
||||
dem der Graph *abgefragt* statt nur für die Neuberechnung *benutzt* wird –
|
||||
die Datenquelle des interaktiven Abhängigkeiten-Panels (`src/store/
|
||||
document.ts`s `cellGraphNeighbors`/`cellGraphSelf`, `src/components/
|
||||
DependencyGraphView.vue`, s. Haupt-README „Abhängigkeiten erkunden"). Rein
|
||||
lesend, ohne jeden Effekt auf Auswertung/Neuberechnung; bleiben auch für
|
||||
eine Zelle auf einem Zirkelbezug korrekt (die Kante existiert im Graphen
|
||||
unabhängig davon, dass `topoOrder` sie aus der Auswertung ausschließt).
|
||||
|
||||
## Fehlermodell
|
||||
|
||||
| Wert | Bedeutung |
|
||||
|
||||
@@ -193,6 +193,71 @@ describe('Workbook named ranges', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workbook graph queries (graphPrecedents/graphDependents)', () => {
|
||||
const coords = (a1: string) => {
|
||||
const m = /^([A-Z]+)([0-9]+)$/.exec(a1)!;
|
||||
return { col: m[1].split('').reduce((n, ch) => n * 26 + (ch.charCodeAt(0) - 64), 0) - 1, row: Number(m[2]) - 1 };
|
||||
};
|
||||
|
||||
it('graphPrecedents lists exactly the cells a formula reads, same-table', () => {
|
||||
const wb = fixture();
|
||||
const { col, row } = coords('D2'); // Übersicht!D2 = B2-C2
|
||||
const precs = wb.graphPrecedents('s-ueber', col, row);
|
||||
expect(precs).toHaveLength(2);
|
||||
expect(precs).toContainEqual({ tableId: 's-ueber', ...coords('B2') });
|
||||
expect(precs).toContainEqual({ tableId: 's-ueber', ...coords('C2') });
|
||||
});
|
||||
|
||||
it('graphPrecedents follows a cross-table range reference to every cell in it', () => {
|
||||
const wb = fixture();
|
||||
const { col, row } = coords('B2'); // Übersicht!B2 = SUM(Umsatz!B2:E2)
|
||||
const precs = wb.graphPrecedents('s-ueber', col, row);
|
||||
expect(precs).toHaveLength(4);
|
||||
for (const a1 of ['B2', 'C2', 'D2', 'E2']) {
|
||||
expect(precs).toContainEqual({ tableId: 's-umsatz', ...coords(a1) });
|
||||
}
|
||||
});
|
||||
|
||||
it('graphDependents is the exact reverse of graphPrecedents', () => {
|
||||
const wb = fixture();
|
||||
const { col, row } = coords('B2'); // Übersicht!B2 -- read by D2 and (via D2) E2 directly reads B2 too
|
||||
const deps = wb.graphDependents('s-ueber', col, row);
|
||||
expect(deps).toContainEqual({ tableId: 's-ueber', ...coords('D2') });
|
||||
expect(deps).toContainEqual({ tableId: 's-ueber', ...coords('E2') }); // E2 = D2/B2*100, reads B2 directly
|
||||
});
|
||||
|
||||
it('a cell with no formula (or a literal) has no precedents', () => {
|
||||
const wb = fixture();
|
||||
const { col, row } = coords('B2'); // Umsatz!B2 is a plain literal, "1200"
|
||||
expect(wb.graphPrecedents('s-umsatz', col, row)).toEqual([]);
|
||||
});
|
||||
|
||||
it('a cell nothing reads has no dependents', () => {
|
||||
const wb = fixture();
|
||||
const { col, row } = coords('A2'); // Umsatz!A2 = "Nord", never referenced by a formula
|
||||
expect(wb.graphDependents('s-umsatz', col, row)).toEqual([]);
|
||||
});
|
||||
|
||||
it('stays accurate for cells on a circular reference (edges exist even though topoOrder drops them)', () => {
|
||||
const a = sheet('s1', 'S1', { A1: '=B1', B1: '=A1' });
|
||||
const wb = new Workbook([a]);
|
||||
const a1 = coords('A1');
|
||||
expect(wb.graphPrecedents('s1', a1.col, a1.row)).toContainEqual({ tableId: 's1', ...coords('B1') });
|
||||
expect(wb.graphDependents('s1', a1.col, a1.row)).toContainEqual({ tableId: 's1', ...coords('B1') });
|
||||
});
|
||||
|
||||
it('updates live after a cell is re-edited to reference something else', () => {
|
||||
const wb = fixture();
|
||||
const c1 = coords('C1');
|
||||
wb.setCell('s-umsatz', c1.col, c1.row, '=B2');
|
||||
expect(wb.graphPrecedents('s-umsatz', c1.col, c1.row)).toContainEqual({ tableId: 's-umsatz', ...coords('B2') });
|
||||
wb.setCell('s-umsatz', c1.col, c1.row, '=B3');
|
||||
const precs = wb.graphPrecedents('s-umsatz', c1.col, c1.row);
|
||||
expect(precs).toContainEqual({ tableId: 's-umsatz', ...coords('B3') });
|
||||
expect(precs).not.toContainEqual({ tableId: 's-umsatz', ...coords('B2') });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workbook recalculation order', () => {
|
||||
it('evaluates precedents before dependents (chain across sheets)', () => {
|
||||
const wb = fixture();
|
||||
|
||||
@@ -62,6 +62,7 @@ export {
|
||||
ERROR_CODES,
|
||||
type ErrorCode,
|
||||
type CellValue,
|
||||
type CellRef,
|
||||
type Scalar,
|
||||
type EngineHost,
|
||||
type NamedRangeDef,
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
isError,
|
||||
keyOf,
|
||||
parseKey,
|
||||
type CellRef,
|
||||
type CellValue,
|
||||
type EngineHost,
|
||||
type NamedRangeDef,
|
||||
@@ -158,6 +159,21 @@ export class Workbook implements EngineHost {
|
||||
return v === undefined ? null : v;
|
||||
}
|
||||
|
||||
/** Direct precedents of a cell -- the cells its own formula reads (a
|
||||
* formula's `#REF`/named-range/etc. resolution already baked into the
|
||||
* graph edges, see `precedentsOf()` above). Public wrapper around the
|
||||
* internal `DependencyGraph`, for UI features (the dependency-graph
|
||||
* view) that need to *query* the graph -- evaluation itself never calls
|
||||
* this, it only calls `setPrecedents`/`affected`/`topoOrder`. */
|
||||
graphPrecedents(tableId: string, col: number, row: number): CellRef[] {
|
||||
return [...this.graph.getPrecedents(keyOf(tableId, col, row))].map(parseKey);
|
||||
}
|
||||
/** Direct dependents of a cell -- every cell whose formula reads *this*
|
||||
* one. See graphPrecedents() above. */
|
||||
graphDependents(tableId: string, col: number, row: number): CellRef[] {
|
||||
return [...this.graph.getDependents(keyOf(tableId, col, row))].map(parseKey);
|
||||
}
|
||||
|
||||
// ---- writes ---------------------------------------------------------
|
||||
|
||||
/** Set (or clear, with `''`) one cell and recalculate the affected cells. */
|
||||
|
||||
Reference in New Issue
Block a user