diff --git a/backend/resources/app/templates/graph-console.tmpl b/backend/resources/app/templates/graph-console.tmpl
index 630ac200ef..52c4d1b5c8 100644
--- a/backend/resources/app/templates/graph-console.tmpl
+++ b/backend/resources/app/templates/graph-console.tmpl
@@ -90,7 +90,7 @@ Graph Console
- {% endif %}
- {% if error %}
-
- {% endif %}
+
+ {% if error %}
+
+ {% endif %}
- {% if query-result %}
-
+ {% endif %}
+
{% endif %}
@@ -302,6 +304,77 @@ Graph Console
});
}
+ function escapeHtml(text) {
+ return String(text)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+ }
+
+ function renderQueryOutput(data) {
+ const output = document.getElementById("graph-query-output");
+ if (!output) return;
+
+ if (data.error) {
+ output.innerHTML =
+ ""
+ + "" + escapeHtml(data.error) + "
";
+ return;
+ }
+
+ const result = data["query-result"];
+ if (!result) {
+ output.innerHTML = "";
+ return;
+ }
+
+ const truncated = result["truncated?"] ? ", truncated" : "";
+ let html =
+ ""
+ + ""
+ + "";
+
+ (result.columns || []).forEach(function (column) {
+ html += "| " + escapeHtml(column) + " | ";
+ });
+ html += "
";
+
+ (result.rows || []).forEach(function (row) {
+ html += "";
+ row.forEach(function (cell) {
+ html += "" + escapeHtml(cell) + " | ";
+ });
+ html += "
";
+ });
+
+ html += "
";
+ output.innerHTML = html;
+ }
+
+ const queryForm = document.getElementById("graph-query-form");
+ if (queryForm) {
+ queryForm.addEventListener("submit", function (event) {
+ event.preventDefault();
+ const formData = new FormData(queryForm);
+ fetch("/dbg/actions/graph-query", {
+ method: "POST",
+ headers: { "Accept": "application/json" },
+ body: formData
+ })
+ .then(function (resp) { return resp.text(); })
+ .then(function (text) {
+ renderQueryOutput(parseTransitMap(JSON.parse(text)));
+ })
+ .catch(function (err) {
+ renderQueryOutput({ error: String(err) });
+ });
+ });
+ }
+
connect();
refreshSyncStatus();
diff --git a/backend/src/app/graph/sync.clj b/backend/src/app/graph/sync.clj
index 7d281e0451..221481586b 100644
--- a/backend/src/app/graph/sync.clj
+++ b/backend/src/app/graph/sync.clj
@@ -408,7 +408,9 @@
{:index (reduce index-remove-shape! index to-delete)
:statements statements
:applied? true})
- {:index index :statements [] :applied? false :reason :missing-shape}))
+ ;; Penpot emits one :del-obj per selected shape; an earlier change in the
+ ;; same batch may have already removed this node (e.g. parent + child).
+ {:index index :statements [] :applied? true}))
(defn- apply-add-page
[index {:keys [id name page]}]
diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj
index 125ef49318..61a0bf7328 100644
--- a/backend/src/app/http/debug.clj
+++ b/backend/src/app/http/debug.clj
@@ -403,21 +403,38 @@
::yres/headers {"content-type" "application/json; charset=utf-8"}
::yres/body (t/encode-str {:error "no-session"} {:type :json-verbose})}))
+(defn- json-request?
+ [request]
+ (some-> request
+ (yreq/get-header "accept")
+ (str/includes? "application/json")))
+
(defn graph-query-handler
- [_cfg {:keys [params ::session/profile-id]}]
+ [_cfg {:keys [params ::session/profile-id] :as request}]
(let [query (:query params)]
(try
(let [result (graph.debug/query-session! profile-id query)]
- (graph-console-response profile-id
- (graph.debug/console-context profile-id
- :query query
- :query-result result)))
+ (if (json-request? request)
+ {::yres/status 200
+ ::yres/headers {"content-type" "application/json; charset=utf-8"}
+ ::yres/body (t/encode-str {:query query
+ :query-result result}
+ {:type :json-verbose})}
+ (graph-console-response profile-id
+ (graph.debug/console-context profile-id
+ :query query
+ :query-result result))))
(catch Throwable e
- (graph-console-response profile-id
- (graph.debug/console-context profile-id
- :query query
- :error (or (:hint (ex-data e))
- (ex-message e))))))))
+ (let [error (or (:hint (ex-data e)) (ex-message e))]
+ (if (json-request? request)
+ {::yres/status 200
+ ::yres/headers {"content-type" "application/json; charset=utf-8"}
+ ::yres/body (t/encode-str {:query query :error error}
+ {:type :json-verbose})}
+ (graph-console-response profile-id
+ (graph.debug/console-context profile-id
+ :query query
+ :error error))))))))
(defn import-handler
[{:keys [::db/pool] :as cfg} {:keys [params ::session/profile-id] :as request}]