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:
@@ -1233,6 +1233,145 @@ describe('insertRow/deleteRow/insertColumn/deleteColumn (mid-grid, not just edge
|
||||
});
|
||||
});
|
||||
|
||||
describe('dependency-graph view (cellGraphSelf/cellGraphNeighbors)', () => {
|
||||
it('cellGraphSelf describes a formula cell: label, raw text, value, table/sheet name', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
setCell(s, name, 'B5', '10');
|
||||
setCell(s, name, 'C1', '=B5+1');
|
||||
const { col, row } = coords('C1');
|
||||
const node = s.cellGraphSelf(id, col, row)!;
|
||||
expect(node).toMatchObject({
|
||||
tableId: id,
|
||||
tableName: name,
|
||||
col,
|
||||
row,
|
||||
label: 'C1',
|
||||
raw: '=B5+1',
|
||||
value: 11,
|
||||
isCircular: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('cellGraphNeighbors.precedents lists exactly the cells a formula reads', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
setCell(s, name, 'B2', '1');
|
||||
setCell(s, name, 'B3', '2');
|
||||
setCell(s, name, 'D1', '=SUM(B2:B3)');
|
||||
const { col, row } = coords('D1');
|
||||
const { precedents } = s.cellGraphNeighbors(id, col, row);
|
||||
expect(precedents.map((n) => n.label).sort()).toEqual(['B2', 'B3']);
|
||||
expect(precedents.every((n) => n.tableId === id)).toBe(true);
|
||||
});
|
||||
|
||||
it('cellGraphNeighbors.dependents is the reverse: who reads this cell', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
setCell(s, name, 'A1', '5');
|
||||
setCell(s, name, 'B1', '=A1*2');
|
||||
setCell(s, name, 'C1', '=A1+1');
|
||||
const { col, row } = coords('A1');
|
||||
const { dependents } = s.cellGraphNeighbors(id, col, row);
|
||||
expect(dependents.map((n) => n.label).sort()).toEqual(['B1', 'C1']);
|
||||
});
|
||||
|
||||
it('follows a cross-table reference, labelling with the other table/sheet name', () => {
|
||||
const s = fresh();
|
||||
const src = s.addTable(s.doc.activeSheetId);
|
||||
s.renameObject(src, 'Quelle4');
|
||||
const dst = s.addTable(s.doc.activeSheetId);
|
||||
s.renameObject(dst, 'Ziel4');
|
||||
setCell(s, 'Quelle4', 'B2', '42');
|
||||
setCell(s, 'Ziel4', 'A1', '=Quelle4!B2');
|
||||
const { col, row } = coords('A1');
|
||||
const { precedents } = s.cellGraphNeighbors(dst, col, row);
|
||||
expect(precedents).toHaveLength(1);
|
||||
expect(precedents[0]).toMatchObject({ tableId: src, tableName: 'Quelle4', label: 'B2', value: 42 });
|
||||
});
|
||||
|
||||
it('flags a cell on a circular reference via isCircular', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
setCell(s, name, 'A1', '=B1');
|
||||
setCell(s, name, 'B1', '=A1');
|
||||
const { col, row } = coords('A1');
|
||||
expect(s.cellGraphSelf(id, col, row)!.isCircular).toBe(true);
|
||||
// the edge itself is still reported even though the cell is circular
|
||||
const { precedents } = s.cellGraphNeighbors(id, col, row);
|
||||
expect(precedents.map((n) => n.label)).toEqual(['B1']);
|
||||
});
|
||||
|
||||
it('a literal (non-formula) cell has no precedents; an unreferenced cell has no dependents', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
setCell(s, name, 'A1', '5');
|
||||
const { col, row } = coords('A1');
|
||||
const { precedents, dependents } = s.cellGraphNeighbors(id, col, row);
|
||||
expect(precedents).toEqual([]);
|
||||
expect(dependents).toEqual([]);
|
||||
});
|
||||
|
||||
it('reflects a re-edit (e.g. after changing which cell a formula reads)', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
setCell(s, name, 'B2', '1');
|
||||
setCell(s, name, 'B3', '2');
|
||||
setCell(s, name, 'C1', '=B2');
|
||||
const { col, row } = coords('C1');
|
||||
expect(s.cellGraphNeighbors(id, col, row).precedents.map((n) => n.label)).toEqual(['B2']);
|
||||
setCell(s, name, 'C1', '=B3');
|
||||
expect(s.cellGraphNeighbors(id, col, row).precedents.map((n) => n.label)).toEqual(['B3']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('openDependencyGraph/closeDependencyGraph (the drawer, not just the data)', () => {
|
||||
it('opens the drawer centered on the given cell', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const { col, row } = coords('B2');
|
||||
expect(s.ui.depGraph).toBeNull();
|
||||
s.openDependencyGraph(id, col, row);
|
||||
expect(s.ui.depGraph).toEqual({ tableId: id, col, row });
|
||||
});
|
||||
|
||||
it('also really selects the cell -- the rest of the app (selection) reflects where you navigated', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const { col, row } = coords('C3');
|
||||
s.openDependencyGraph(id, col, row);
|
||||
expect(s.doc.selection).toMatchObject({ tableId: id, anchor: { col, row }, focus: { col, row } });
|
||||
expect(s.doc.activeObjectId).toBe(id);
|
||||
});
|
||||
|
||||
it('switches the active sheet tab when navigating to a cell on a different sheet', () => {
|
||||
const s = fresh();
|
||||
const otherSheet = s.addSheet();
|
||||
const id = s.addTable(otherSheet);
|
||||
expect(s.doc.activeSheetId).toBe(otherSheet); // addTable already switched here
|
||||
s.setActiveSheet(s.doc.sheets[0].id); // move away
|
||||
expect(s.doc.activeSheetId).not.toBe(otherSheet);
|
||||
|
||||
s.openDependencyGraph(id, 0, 0);
|
||||
expect(s.doc.activeSheetId).toBe(otherSheet); // navigated back automatically
|
||||
});
|
||||
|
||||
it('closeDependencyGraph clears it without touching the current selection', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
s.openDependencyGraph(id, 0, 0);
|
||||
s.closeDependencyGraph();
|
||||
expect(s.ui.depGraph).toBeNull();
|
||||
expect(s.doc.selection).toMatchObject({ tableId: id, focus: { col: 0, row: 0 } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('functionSuggestions (formula autocomplete)', () => {
|
||||
it('filters built-ins by prefix, case-insensitively', () => {
|
||||
const s = fresh();
|
||||
|
||||
Reference in New Issue
Block a user