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
Cypher query -
+
@@ -99,37 +99,39 @@ Graph Console
- {% endif %} - {% if error %} -
- Error -
{{error}}
-
- {% endif %} +
+ {% if error %} +
+ Error +
{{error}}
+
+ {% endif %} - {% if query-result %} -
- Results ({{query-result.row-count}} rows{% if query-result.truncated? %}, truncated{% endif %}) - - - - {% for column in query-result.columns %} - + {% if query-result %} +
+ Results ({{query-result.row-count}} rows{% if query-result.truncated? %}, truncated{% endif %}) +
{{column}}
+ + + {% for column in query-result.columns %} + + {% endfor %} + + + + {% for row in query-result.rows %} + + {% for cell in row %} + + {% endfor %} + {% endfor %} - - - - {% for row in query-result.rows %} - - {% for cell in row %} - - {% endfor %} - - {% endfor %} - -
{{column}}
{{cell}}
{{cell}}
-
+ + + + {% 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 = + "
Error" + + "
" + escapeHtml(data.error) + "
"; + return; + } + + const result = data["query-result"]; + if (!result) { + output.innerHTML = ""; + return; + } + + const truncated = result["truncated?"] ? ", truncated" : ""; + let html = + "
Results (" + + escapeHtml(String(result["row-count"])) + + " rows" + truncated + ")" + + "" + + ""; + + (result.columns || []).forEach(function (column) { + html += ""; + }); + html += ""; + + (result.rows || []).forEach(function (row) { + html += ""; + row.forEach(function (cell) { + html += ""; + }); + html += ""; + }); + + html += "
" + escapeHtml(column) + "
" + escapeHtml(cell) + "
"; + 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}]