mirror of
https://github.com/penpot/penpot.git
synced 2026-08-08 22:08:39 +00:00
🐛 Let the graph view's query filter follow the graph
"Show result in graph view" froze the set of node ids the query returned and filtered every later repaint against it. Live sync creates ids the set has never seen, so a shape created while a filter was on could not appear in the view at any point, and clicking "Show full graph" was the only way to see it. A node the query would no longer match stayed. Keep the query beside the ids and re-run it whenever the graph repaints, which is only when the projection actually changed. A failed re-run keeps the ids in hand and says so on the status line rather than passing a stale view off as current. `idsInResult` and `presentIds` are extracted from the two places that scraped UUIDs out of a result. Verified in the devenv: with a filter showing 108 of 276 nodes, a `:file-change` adding a Frame published on the session's msgbus topic took the view to 109 of 277, with the new node carrying its added mark, and no interaction. AI-assisted-by: mixed models
This commit is contained in:
parent
474fb58e20
commit
dbbd70e68e
@ -627,6 +627,13 @@ Graph Console
|
||||
let lastGraphSig = null;
|
||||
let renderForced = false;
|
||||
let graphFilterIds = null;
|
||||
// The query that produced the filter, kept so the filter can follow the
|
||||
// graph. A filter is a set of node ids, and live sync creates ids the set
|
||||
// has never seen, so a frozen set hides every node created after the query
|
||||
// ran. Re-running the query on each repaint is what keeps a filtered view
|
||||
// honest; the cadence is the repaint's, and a repaint is skipped when the
|
||||
// projection is unchanged.
|
||||
let graphFilterQuery = null;
|
||||
// Node tables hidden via legend clicks (session-local, not persisted).
|
||||
const hiddenTables = new Set();
|
||||
|
||||
@ -1051,18 +1058,59 @@ Graph Console
|
||||
revn: lastGraphData.revn, truncated: lastGraphData.truncated };
|
||||
}
|
||||
|
||||
function applyQueryFilter(ids) {
|
||||
if (!lastGraphData) return;
|
||||
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
|
||||
|
||||
function idsInResult(result) {
|
||||
const ids = new Set();
|
||||
(result.rows || []).forEach(function (row) {
|
||||
row.forEach(function (cell) {
|
||||
const found = String(cell).match(UUID_RE);
|
||||
if (found) found.forEach(function (m) { ids.add(m.toLowerCase()); });
|
||||
});
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
||||
function presentIds(ids) {
|
||||
const present = new Set();
|
||||
lastGraphData.nodes.forEach(function (n) { if (ids.has(n.id)) present.add(n.id); });
|
||||
return present;
|
||||
}
|
||||
|
||||
function applyQueryFilter(ids, query) {
|
||||
if (!lastGraphData) return;
|
||||
const present = presentIds(ids);
|
||||
if (!present.size) {
|
||||
graphViewStatus.textContent = "query result matches no nodes in the loaded graph";
|
||||
return;
|
||||
}
|
||||
graphFilterIds = present;
|
||||
graphFilterQuery = query || null;
|
||||
renderCurrent();
|
||||
}
|
||||
|
||||
function renderCurrentRefiltered() {
|
||||
if (!graphFilterQuery) {
|
||||
renderCurrent();
|
||||
return;
|
||||
}
|
||||
// A stale filter is worse than a slow one: it hides the very node the
|
||||
// change created. Re-run, and on failure keep the ids in hand and say so
|
||||
// rather than pass a filtered-out graph off as current.
|
||||
runConsoleQuery(graphFilterQuery)
|
||||
.then(function (data) {
|
||||
if (data && data.error) throw new Error(data.error);
|
||||
const result = data && data["query-result"];
|
||||
if (result) graphFilterIds = presentIds(idsInResult(result));
|
||||
renderCurrent();
|
||||
})
|
||||
.catch(function (err) {
|
||||
renderCurrent();
|
||||
graphViewStatus.textContent +=
|
||||
" — filter query failed, ids are from its last run: " + err;
|
||||
});
|
||||
}
|
||||
|
||||
function renderCurrent() {
|
||||
if (!lastGraphData) return;
|
||||
const data = filteredGraphData();
|
||||
@ -1142,7 +1190,7 @@ Graph Console
|
||||
markDiff(lastGraphData, data);
|
||||
lastGraphData = data;
|
||||
lastGraphSig = sig;
|
||||
renderCurrent();
|
||||
renderCurrentRefiltered();
|
||||
schedulePulses();
|
||||
})
|
||||
.catch(function (err) {
|
||||
@ -1485,20 +1533,16 @@ Graph Console
|
||||
output.innerHTML = html;
|
||||
|
||||
// Offer to view the result as an induced subgraph: any UUID appearing
|
||||
// in any result cell selects that node in the loaded graph.
|
||||
const uuidRe = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
|
||||
const ids = new Set();
|
||||
(result.rows || []).forEach(function (row) {
|
||||
row.forEach(function (cell) {
|
||||
const found = String(cell).match(uuidRe);
|
||||
if (found) found.forEach(function (m) { ids.add(m.toLowerCase()); });
|
||||
});
|
||||
});
|
||||
// in any result cell selects that node in the loaded graph. The query
|
||||
// goes with the ids, so the view can re-run it and follow the graph.
|
||||
const ids = idsInResult(result);
|
||||
if (ids.size) {
|
||||
const queryEl = document.querySelector("#graph-query-form textarea[name=query]");
|
||||
const query = queryEl ? queryEl.value : null;
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.textContent = "Show result in graph view (" + ids.size + " ids)";
|
||||
btn.addEventListener("click", function () { applyQueryFilter(ids); });
|
||||
btn.addEventListener("click", function () { applyQueryFilter(ids, query); });
|
||||
document.getElementById("graph-result-actions").appendChild(btn);
|
||||
}
|
||||
}
|
||||
@ -1681,6 +1725,7 @@ Graph Console
|
||||
if (filterResetBtn) {
|
||||
filterResetBtn.addEventListener("click", function () {
|
||||
graphFilterIds = null;
|
||||
graphFilterQuery = null;
|
||||
renderCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user