1
0
Эх сурвалжийг харах

Merge branch 'master' into feat/capacitor-new

charlie 4 сар өмнө
parent
commit
08b824934c
27 өөрчлөгдсөн 579 нэмэгдсэн , 418 устгасан
  1. 7 10
      clj-e2e/src/logseq/e2e/graph.clj
  2. 1 0
      clj-e2e/src/logseq/e2e/keyboard.clj
  3. 11 0
      clj-e2e/test/logseq/e2e/outliner_basic_test.clj
  4. 9 3
      deps/db/src/logseq/db/frontend/malli_schema.cljs
  5. 2 2
      deps/db/src/logseq/db/frontend/property/type.cljs
  6. 222 159
      deps/graph-parser/src/logseq/graph_parser/exporter.cljs
  7. 28 4
      deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs
  8. 16 1
      deps/graph-parser/test/resources/exporter-test-graph/journals/2024_08_07.md
  9. 6 1
      deps/graph-parser/test/resources/exporter-test-graph/journals/2025_06_12.md
  10. 4 0
      deps/graph-parser/test/resources/exporter-test-graph/journals/2025_06_23.md
  11. 27 47
      deps/outliner/src/logseq/outliner/validate.cljs
  12. 30 8
      deps/outliner/test/logseq/outliner/validate_test.cljs
  13. 2 0
      public/index.html
  14. 2 0
      resources/index.html
  15. 121 110
      src/main/frontend/components/block.cljs
  16. 12 3
      src/main/frontend/components/block.css
  17. 14 9
      src/main/frontend/components/editor.cljs
  18. 20 12
      src/main/frontend/components/property/value.cljs
  19. 4 2
      src/main/frontend/components/repo.cljs
  20. 6 10
      src/main/frontend/extensions/lightbox.cljs
  21. 2 2
      src/main/frontend/extensions/pdf/assets.cljs
  22. 1 1
      src/main/frontend/handler/db_based/page.cljs
  23. 4 1
      src/main/frontend/handler/editor.cljs
  24. 25 31
      src/main/frontend/worker/db/migrate.cljs
  25. 1 1
      src/main/frontend/worker/db_worker.cljs
  26. 1 0
      src/resources/dicts/en.edn
  27. 1 1
      src/test/frontend/test/frontend_node_test_runner.cljs

+ 7 - 10
clj-e2e/src/logseq/e2e/graph.clj

@@ -2,9 +2,9 @@
   (:require [clojure.edn :as edn]
             [clojure.string :as string]
             [logseq.e2e.assert :as assert]
+            [logseq.e2e.locator :as loc]
             [logseq.e2e.util :as util]
-            [wally.main :as w]
-            [logseq.e2e.locator :as loc]))
+            [wally.main :as w]))
 
 (defn- refresh-all-remote-graphs
   []
@@ -39,14 +39,11 @@
 (defn remove-remote-graph
   [graph-name]
   (wait-for-remote-graph graph-name)
-  (let [local-unlink-button-q
-        (.first (w/-query (format "div[data-testid='logseq_db_%s'] a:has-text(\"Unlink (local)\")" graph-name)))]
-    (if (.isVisible local-unlink-button-q)
-      (do (w/click local-unlink-button-q)
-          (w/click "div[role='alertdialog'] button:text('ok')")
-          (remove-remote-graph graph-name))
-      (do (w/click (format "div[data-testid='logseq_db_%s'] a:has-text(\"Remove (server)\")" graph-name))
-          (w/click "div[role='alertdialog'] button:text('ok')")))))
+  (let [action-btn
+        (.first (w/-query (format "div[data-testid='logseq_db_%s'] .graph-action-btn" graph-name)))]
+    (w/click action-btn)
+    (w/click ".delete-remote-graph-menu-item")
+    (w/click "div[role='alertdialog'] button:text('ok')")))
 
 (defn switch-graph
   [to-graph-name wait-sync?]

+ 1 - 0
clj-e2e/src/logseq/e2e/keyboard.clj

@@ -8,6 +8,7 @@
 (def enter #(press "Enter"))
 (def esc #(press "Escape"))
 (def backspace #(press "Backspace"))
+(def delete #(press "Delete"))
 (def tab #(press "Tab"))
 (def shift+tab #(press "Shift+Tab"))
 (def shift+enter #(press "Shift+Enter"))

+ 11 - 0
clj-e2e/test/logseq/e2e/outliner_basic_test.clj

@@ -65,6 +65,14 @@
     (is (= "b1" (util/get-edit-content)))
     (is (= 1 (util/page-blocks-count)))))
 
+(defn delete-end []
+  (testing "Delete at end"
+    (b/new-blocks ["b1" "b2" "b3"])
+    (k/arrow-up)
+    (k/delete)
+    (is (= "b2b3" (util/get-edit-content)))
+    (is (= 2 (util/page-blocks-count)))))
+
 (defn delete-test-with-children []
   (testing "Delete block with its children"
     (b/new-blocks ["b1" "b2" "b3" "b4"])
@@ -88,5 +96,8 @@
 (deftest delete-test
   (delete))
 
+(deftest delete-end-test
+  (delete-end))
+
 (deftest delete-test-with-children-test
   (delete-test-with-children))

+ 9 - 3
deps/db/src/logseq/db/frontend/malli_schema.cljs

@@ -223,9 +223,15 @@
    [:multi {:dispatch #(-> % first :logseq.property/type)}]
    (map (fn [[prop-type value-schema]]
           [prop-type
-           (let [schema-fn (if (vector? value-schema) (last value-schema) value-schema)]
-             [:fn (fn [tuple]
-                    (validate-property-value *db-for-validate-fns* schema-fn tuple))])])
+           (let [schema-fn (if (vector? value-schema) (last value-schema) value-schema)
+                 error-message (when (vector? value-schema)
+                                 (and (map? (second value-schema))
+                                      (:error/message (second value-schema))))]
+             [:fn
+              (when error-message
+                {:error/message error-message})
+              (fn [tuple]
+                (validate-property-value *db-for-validate-fns* schema-fn tuple))])])
         db-property-type/built-in-validation-schemas)))
 
 (def block-properties

+ 2 - 2
deps/db/src/logseq/db/frontend/property/type.cljs

@@ -76,12 +76,12 @@
 ;; Validate && list fixes for non-validated values when updating property schema
 
 (defn url?
-  "Test if it is a `protocol://`-style URL.
+  "Test if it is a `protocol://`-style URL. Allows custom protocol such as `zotero`.
    Originally from common-util/url? but does not need to be the same"
   [s]
   (and (string? s)
        (try
-         (not (contains? #{nil "null"} (.-origin (js/URL. s))))
+         (not (contains? #{nil} (.-origin (js/URL. s))))
          (catch :default _e
            false))))
 

+ 222 - 159
deps/graph-parser/src/logseq/graph_parser/exporter.cljs

@@ -13,6 +13,7 @@
             [logseq.common.config :as common-config]
             [logseq.common.path :as path]
             [logseq.common.util :as common-util]
+            [logseq.common.util.block-ref :as block-ref]
             [logseq.common.util.date-time :as date-time-util]
             [logseq.common.util.macro :as macro-util]
             [logseq.common.util.namespace :as ns-util]
@@ -457,6 +458,8 @@
   "Special keywords in previous query table"
   {:page :block/title
    :block :block/title
+   :tags :block/tags
+   :alias :block/alias
    :created-at :block/created-at
    :updated-at :block/updated-at})
 
@@ -795,60 +798,140 @@
         (pr-str (dissoc query-map :title :group-by-page? :collapsed?))
         query-str))))
 
+(defn- ast->text
+  "Given an ast block, convert it to text for use as a block title. This is a
+  slimmer version of handler.export.text/export-blocks-as-markdown"
+  [ast-block {:keys [log-fn]
+              :or {log-fn prn}}]
+  (let [extract
+        (fn extract [node]
+          (let [extract-emphasis
+                (fn extract-emphasis [node]
+                  (let [[[type'] coll'] node]
+                    (case type'
+                      "Bold"
+                      (vec (concat ["**"] (mapcat extract coll') ["**"]))
+                      "Italic"
+                      (vec (concat ["*"] (mapcat extract coll') ["*"]))
+                      "Strike_through"
+                      (vec (concat ["~~"] (mapcat extract coll') ["~~"]))
+                      "Highlight"
+                      (vec (concat ["^^"] (mapcat extract coll') ["^^"]))
+                      (throw (ex-info (str "Failed to wrap Emphasis AST block of type " (pr-str type')) {})))))]
+            (cond
+              (and (vector? node) (#{"Inline_Html" "Plain" "Inline_Hiccup"} (first node)))
+              [(second node)]
+              (and (vector? node) (#{"Break_Line" "Hard_Break_Line"} (first node)))
+              ["\n"]
+              (and (vector? node) (= (first node) "Link"))
+              [(:full_text (second node))]
+              (and (vector? node) (#{"Paragraph" "Quote"} (first node)))
+              (mapcat extract (second node))
+              (and (vector? node) (= (first node) "Tag"))
+              (into ["#"] (mapcat extract (second node)))
+              (and (vector? node) (= (first node) "Emphasis"))
+              (extract-emphasis (second node))
+              (and (vector? node) (= ["Custom" "query"] (take 2 node)))
+              [(get node 4)]
+              (and (vector? node) (= (first node) "Code"))
+              ["`" (second node) "`"]
+              (and (vector? node) (= "Macro" (first node)) (= "query" (:name (second node))))
+              (:arguments (second node))
+              (and (vector? node) (= (first node) "Example"))
+              (second node)
+              :else
+              (do
+                (log-fn :ast->text "Ignored ast node" :node node)
+                []))))]
+    (->> (extract ast-block)
+        ;;  ((fn [x] (prn :X x) x))
+         (apply str)
+         string/trim)))
+
+(defn- walk-ast-blocks
+  "Walks each ast block in order to its full depth. Saves multiple ast types for
+  use in build-block-tx. This walk is only done once for perf reasons"
+  [ast-blocks]
+  (let [results (atom {:simple-queries []
+                       :asset-links []
+                       :embeds []})]
+    (walk/prewalk
+     (fn [x]
+       (cond
+         (and (vector? x)
+              (= "Link" (first x))
+              (common-config/local-asset? (second (:url (second x)))))
+         (swap! results update :asset-links conj x)
+         (and (vector? x)
+              (= "Macro" (first x))
+              (= "embed" (:name (second x))))
+         (swap! results update :embeds conj x)
+         (and (vector? x)
+              (= "Macro" (first x))
+              (= "query" (:name (second x))))
+         (swap! results update :simple-queries conj x))
+       x)
+     ast-blocks)
+    @results))
+
+(defn- handle-queries
+  "If a block contains a simple or advanced queries, converts block to a #Query node"
+  [{:block/keys [title] :as block} db page-names-to-uuids walked-ast-blocks options]
+  (if-let [query (some-> (first (:simple-queries walked-ast-blocks))
+                         (ast->text (select-keys options [:log-fn]))
+                         string/trim)]
+    (let [props {:logseq.property/query query}
+          {:keys [block-properties pvalues-tx]}
+          (build-properties-and-values props db page-names-to-uuids
+                                       (select-keys block [:block/properties-text-values :block/name :block/title :block/uuid])
+                                       options)
+          block'
+          (-> (update block :block/tags (fnil conj []) :logseq.class/Query)
+              (merge block-properties
+                     {:block/title (string/trim (string/replace-first title #"\{\{query(.*)\}\}" ""))}))]
+      {:block block'
+       :pvalues-tx pvalues-tx})
+    (if-let [advanced-query (some-> (first (filter #(= ["Custom" "query"] (take 2 %)) (:block.temp/ast-blocks block)))
+                                    (ast->text (select-keys options [:log-fn]))
+                                    string/trim)]
+      (let [props {:logseq.property/query (migrate-advanced-query-string advanced-query)}
+            {:keys [block-properties pvalues-tx]}
+            (build-properties-and-values props db page-names-to-uuids
+                                         (select-keys block [:block/properties-text-values :block/name :block/title :block/uuid])
+                                         options)
+            pvalues-tx'
+            (concat pvalues-tx [{:block/uuid (second (:logseq.property/query block-properties))
+                                 :logseq.property.code/lang "clojure"
+                                 :logseq.property.node/display-type :code}])
+            block'
+            (let [query-map (common-util/safe-read-map-string advanced-query)]
+              (cond-> (update block :block/tags (fnil conj []) :logseq.class/Query)
+                true
+                (merge block-properties)
+                true
+                (assoc :block/title
+                       (or (when-let [title' (:title query-map)]
+                             (if (string? title') title' (pr-str title')))
+                           ;; Put all non-query content in title for now
+                           (string/trim (string/replace-first title #"(?s)#\+BEGIN_QUERY(.*)#\+END_QUERY" ""))))
+                (:collapsed? query-map)
+                (assoc :block/collapsed? true)))]
+        {:block block'
+         :pvalues-tx pvalues-tx'})
+      {:block block})))
+
 (defn- handle-block-properties
   "Does everything page properties does and updates a couple of block specific attributes"
-  [{:block/keys [title] :as block*}
-   db page-names-to-uuids refs
+  [block* db page-names-to-uuids refs walked-ast-blocks
    {{:keys [property-classes]} :user-options :as options}]
   (let [{:keys [block properties-tx]} (handle-page-and-block-properties block* db page-names-to-uuids refs options)
-        advanced-query (some->> (second (re-find #"(?s)#\+BEGIN_QUERY(.*)#\+END_QUERY" title)) string/trim)
-        additional-props (cond-> {}
-                           ;; Order matters as we ensure a simple query gets priority
-                           (macro-util/query-macro? title)
-                           (assoc :logseq.property/query
-                                  (or (some->> (second (re-find #"\{\{query(.*)\}\}" title))
-                                               string/trim)
-                                      title))
-                           (seq advanced-query)
-                           (assoc :logseq.property/query (migrate-advanced-query-string advanced-query)))
-        {:keys [block-properties pvalues-tx]}
-        (when (seq additional-props)
-          (build-properties-and-values additional-props db page-names-to-uuids
-                                       (select-keys block [:block/properties-text-values :block/name :block/title :block/uuid])
-                                       options))
-        pvalues-tx' (if (and pvalues-tx (seq advanced-query))
-                      (concat pvalues-tx [{:block/uuid (second (:logseq.property/query block-properties))
-                                           :logseq.property.code/lang "clojure"
-                                           :logseq.property.node/display-type :code}])
-                      pvalues-tx)]
+        {block' :block :keys [pvalues-tx]} (handle-queries block db page-names-to-uuids walked-ast-blocks options)]
     {:block
-     (cond-> block
-       (seq block-properties)
-       (merge block-properties)
-
-       (macro-util/query-macro? title)
-       ((fn [b]
-          (merge (update b :block/tags (fnil conj []) :logseq.class/Query)
-                 ;; Put all non-query content in title. Could just be a blank string
-                 {:block/title (string/trim (string/replace-first title #"\{\{query(.*)\}\}" ""))})))
-
-       (seq advanced-query)
-       ((fn [b]
-          (let [query-map (common-util/safe-read-map-string advanced-query)]
-            (cond-> (update b :block/tags (fnil conj []) :logseq.class/Query)
-              true
-              (assoc :block/title
-                     (or (when-let [title' (:title query-map)]
-                           (if (string? title') title' (pr-str title')))
-                         ;; Put all non-query content in title for now
-                         (string/trim (string/replace-first title #"(?s)#\+BEGIN_QUERY(.*)#\+END_QUERY" ""))))
-              (:collapsed? query-map)
-              (assoc :block/collapsed? true)))))
-
+     (cond-> block'
        (and (seq property-classes) (seq (:block/refs block*)))
        ;; remove unused, nonexistent property page
        (update :block/refs (fn [refs] (remove #(property-classes (keyword (:block/name %))) refs))))
-     :properties-tx (concat properties-tx (when pvalues-tx' pvalues-tx'))}))
+     :properties-tx (concat properties-tx (when pvalues-tx pvalues-tx))}))
 
 (defn- update-block-refs
   "Updates the attributes of a block ref as this is where a new page is defined. Also
@@ -906,21 +989,6 @@
   [path]
   (re-find #"assets/.*$" path))
 
-(defn- find-all-asset-links
-  "Walks each ast block in order to its full depth as Link asts can be in different
-   locations e.g. a Heading vs a Paragraph ast block"
-  [ast-blocks]
-  (let [results (atom [])]
-    (walk/prewalk
-     (fn [x]
-       (when (and (vector? x)
-                  (= "Link" (first x))
-                  (common-config/local-asset? (second (:url (second x)))))
-         (swap! results conj x))
-       x)
-     ast-blocks)
-    @results))
-
 (defn- update-asset-links-in-block-title [block-title asset-name-to-uuids ignored-assets]
   (reduce (fn [acc [asset-name asset-uuid]]
             (let [new-title (string/replace acc
@@ -938,93 +1006,51 @@
           asset-name-to-uuids))
 
 (defn- handle-assets-in-block
-  [block {:keys [assets ignored-assets]}]
-  (let [asset-links (find-all-asset-links (:block.temp/ast-blocks block))]
-    (if (seq asset-links)
-      (let [asset-maps
-            (keep
-             (fn [asset-link]
-               (let [asset-name (-> asset-link second :url second asset-path->name)]
-                 (if-let [asset-data (and asset-name (get @assets asset-name))]
-                   (if (:block/uuid asset-data)
-                     {:asset-name-uuid [asset-name (:block/uuid asset-data)]}
-                     (let [new-block (sqlite-util/block-with-timestamps
-                                      {:block/uuid (d/squuid)
-                                       :block/order (db-order/gen-key)
-                                       :block/page :logseq.class/Asset
-                                       :block/parent :logseq.class/Asset})
-                           new-asset (merge new-block
-                                            {:block/tags [:logseq.class/Asset]
-                                             :logseq.property.asset/type (:type asset-data)
-                                             :logseq.property.asset/checksum (:checksum asset-data)
-                                             :logseq.property.asset/size (:size asset-data)
-                                             :block/title (db-asset/asset-name->title (node-path/basename asset-name))}
-                                            (when-let [metadata (not-empty (common-util/safe-read-map-string (:metadata (second asset-link))))]
-                                              {:logseq.property.asset/resize-metadata metadata}))]
+  "If a block contains assets, creates them as #Asset nodes in the Asset page and references them in the block."
+  [block {:keys [asset-links]} {:keys [assets ignored-assets]}]
+  (if (seq asset-links)
+    (let [asset-maps
+          (keep
+           (fn [asset-link]
+             (let [asset-name (-> asset-link second :url second asset-path->name)]
+               (if-let [asset-data (and asset-name (get @assets asset-name))]
+                 (if (:block/uuid asset-data)
+                   {:asset-name-uuid [asset-name (:block/uuid asset-data)]}
+                   (let [new-block (sqlite-util/block-with-timestamps
+                                    {:block/uuid (d/squuid)
+                                     :block/order (db-order/gen-key)
+                                     :block/page :logseq.class/Asset
+                                     :block/parent :logseq.class/Asset})
+                         new-asset (merge new-block
+                                          {:block/tags [:logseq.class/Asset]
+                                           :logseq.property.asset/type (:type asset-data)
+                                           :logseq.property.asset/checksum (:checksum asset-data)
+                                           :logseq.property.asset/size (:size asset-data)
+                                           :block/title (db-asset/asset-name->title (node-path/basename asset-name))}
+                                          (when-let [metadata (not-empty (common-util/safe-read-map-string (:metadata (second asset-link))))]
+                                            {:logseq.property.asset/resize-metadata metadata}))]
                       ;;  (prn :asset-added! (node-path/basename asset-name) #_(get @assets asset-name))
                       ;;  (cljs.pprint/pprint asset-link)
-                       (swap! assets assoc-in [asset-name :block/uuid] (:block/uuid new-block))
-                       {:asset-name-uuid [asset-name (:block/uuid new-asset)]
-                        :asset new-asset}))
-                   (do
-                     (swap! ignored-assets conj
-                            {:reason "No asset data found for this asset path"
-                             :path (-> asset-link second :url second)
-                             :location {:block (:block/title block)}})
-                     nil))))
-             asset-links)
-            asset-blocks (keep :asset asset-maps)
-            asset-names-to-uuids
-            (into {} (map :asset-name-uuid asset-maps))]
-        (cond-> {:block
-                 (update block :block/title update-asset-links-in-block-title asset-names-to-uuids ignored-assets)}
-          (seq asset-blocks)
-          (assoc :asset-blocks-tx asset-blocks)))
-      {:block block})))
-
-(defn- ast->text
-  "Given an ast block, convert it to text for use as a block title. This is a
-  slimmer version of handler.export.text/export-blocks-as-markdown"
-  [ast-block {:keys [log-fn]
-              :or {log-fn prn}}]
-  (let [extract
-        (fn extract [node]
-          (let [extract-emphasis
-                (fn extract-emphasis [node]
-                  (let [[[type'] coll'] node]
-                    (case type'
-                      "Bold"
-                      (vec (concat ["**"] (mapcat extract coll') ["**"]))
-                      "Italic"
-                      (vec (concat ["*"] (mapcat extract coll') ["*"]))
-                      "Strike_through"
-                      (vec (concat ["~~"] (mapcat extract coll') ["~~"]))
-                      "Highlight"
-                      (vec (concat ["^^"] (mapcat extract coll') ["^^"]))
-                      (throw (ex-info (str "Failed to wrap Emphasis AST block of type " (pr-str type')) {})))))]
-            (cond
-              (and (vector? node) (#{"Inline_Html" "Plain"} (first node)))
-              [(second node)]
-              (and (vector? node) (#{"Break_Line" "Hard_Break_Line"} (first node)))
-              ["\n"]
-              (and (vector? node) (= (first node) "Link"))
-              [(:full_text (second node))]
-              (and (vector? node) (#{"Paragraph" "Quote"} (first node)))
-              (mapcat extract (second node))
-              (and (vector? node) (= (first node) "Tag"))
-              (into ["#"] (mapcat extract (second node)))
-              (and (vector? node) (= (first node) "Emphasis"))
-              (extract-emphasis (second node))
-              :else
-              (do
-                (log-fn :ast->text "Ignored ast node" :node node)
-                []))))]
-    (->> (extract ast-block)
-        ;;  ((fn [x] (prn :X x) x))
-         (apply str)
-         string/trim)))
-
-(defn- handle-quote-in-block
+                     (swap! assets assoc-in [asset-name :block/uuid] (:block/uuid new-block))
+                     {:asset-name-uuid [asset-name (:block/uuid new-asset)]
+                      :asset new-asset}))
+                 (do
+                   (swap! ignored-assets conj
+                          {:reason "No asset data found for this asset path"
+                           :path (-> asset-link second :url second)
+                           :location {:block (:block/title block)}})
+                   nil))))
+           asset-links)
+          asset-blocks (keep :asset asset-maps)
+          asset-names-to-uuids
+          (into {} (map :asset-name-uuid asset-maps))]
+      (cond-> {:block
+               (update block :block/title update-asset-links-in-block-title asset-names-to-uuids ignored-assets)}
+        (seq asset-blocks)
+        (assoc :asset-blocks-tx asset-blocks)))
+    {:block block}))
+
+(defn- handle-quotes
   "If a block contains a quote, convert block to #Quote node"
   [block opts]
   (if-let [ast-block (first (filter #(= "Quote" (first %)) (:block.temp/ast-blocks block)))]
@@ -1034,15 +1060,41 @@
             :block/tags [:logseq.class/Quote-block]})
     block))
 
+(defn- handle-embeds
+  "If a block contains page or block embeds, converts block to a :block/link based embed"
+  [block page-names-to-uuids {:keys [embeds]} {:keys [log-fn] :or {log-fn prn}}]
+  (if-let [embed-node (first embeds)]
+    (cond
+      (page-ref/page-ref? (str (first (:arguments (second embed-node)))))
+      (let [page-uuid (get-page-uuid page-names-to-uuids
+                                     (some-> (page-ref/get-page-name (first (:arguments (second embed-node))))
+                                             common-util/page-name-sanity-lc)
+                                     {:block block})]
+        (merge block
+               {:block/title ""
+                :block/link [:block/uuid page-uuid]}))
+      (block-ref/block-ref? (str (first (:arguments (second embed-node)))))
+      (let [block-uuid (uuid (block-ref/get-block-ref-id (first (:arguments (second embed-node)))))]
+        (merge block
+               {:block/title ""
+                :block/link [:block/uuid block-uuid]}))
+      :else
+      (do
+        (log-fn :invalid-embed-arguments "Ignore embed because of invalid arguments" :args (:arguments (second embed-node)))
+        block))
+    block))
+
 (defn- build-block-tx
   [db block* pre-blocks {:keys [page-names-to-uuids] :as per-file-state} {:keys [import-state journal-created-ats] :as options}]
   ;; (prn ::block-in block*)
-  (let [;; needs to come before update-block-refs to detect new property schemas
+  (let [walked-ast-blocks (walk-ast-blocks (:block.temp/ast-blocks block*))
+        ;; needs to come before update-block-refs to detect new property schemas
         {:keys [block properties-tx]}
-        (handle-block-properties block* db page-names-to-uuids (:block/refs block*) options)
-        {block-after-built-in-props :block deadline-properties-tx :properties-tx} (update-block-deadline-and-scheduled block page-names-to-uuids options)
+        (handle-block-properties block* db page-names-to-uuids (:block/refs block*) walked-ast-blocks options)
+        {block-after-built-in-props :block deadline-properties-tx :properties-tx}
+        (update-block-deadline-and-scheduled block page-names-to-uuids options)
         {block-after-assets :block :keys [asset-blocks-tx]}
-        (handle-assets-in-block block-after-built-in-props (select-keys import-state [:assets :ignored-assets]))
+        (handle-assets-in-block block-after-built-in-props walked-ast-blocks (select-keys import-state [:assets :ignored-assets]))
         ;; :block/page should be [:block/page NAME]
         journal-page-created-at (some-> (:block/page block*) second journal-created-ats)
         prepared-block (cond-> block-after-assets
@@ -1053,7 +1105,8 @@
                    (fix-block-name-lookup-ref page-names-to-uuids)
                    (update-block-refs page-names-to-uuids options)
                    (update-block-tags db (:user-options options) per-file-state (:all-idents import-state))
-                   (handle-quote-in-block (select-keys options [:log-fn]))
+                   (handle-embeds page-names-to-uuids walked-ast-blocks (select-keys options [:log-fn]))
+                   (handle-quotes (select-keys options [:log-fn]))
                    (update-block-marker options)
                    (update-block-priority options)
                    add-missing-timestamps
@@ -1344,17 +1397,26 @@
   "Separates new pages from new properties tx in preparation for properties to
   be transacted separately. Also builds property pages tx and converts existing
   pages that are now properties"
-  [pages-tx old-properties existing-pages import-state]
+  [pages-tx old-properties existing-pages import-state upstream-properties]
   (let [new-properties (set/difference (set (keys @(:property-schemas import-state))) (set old-properties))
         ;; _ (when (seq new-properties) (prn :new-properties new-properties))
         [properties-tx pages-tx'] ((juxt filter remove)
                                    #(contains? new-properties (keyword (:block/name %))) pages-tx)
         property-pages-tx (map (fn [{block-uuid :block/uuid :block/keys [title]}]
                                  (let [property-name (keyword (string/lower-case title))
-                                       db-ident (get-ident @(:all-idents import-state) property-name)]
-                                   (sqlite-util/build-new-property db-ident
-                                                                   (get-property-schema @(:property-schemas import-state) property-name)
-                                                                   {:title title :block-uuid block-uuid})))
+                                       db-ident (get-ident @(:all-idents import-state) property-name)
+                                       upstream-property (get upstream-properties property-name)]
+                                   (sqlite-util/build-new-property
+                                    db-ident
+                                    ;; Tweak new properties that have upstream changes in flight to behave like
+                                    ;; existing properties i.e. they should be defined by the upstream property
+                                    (if (and upstream-property
+                                             (#{:date :node} (:from-type upstream-property))
+                                             (= :default (get-in upstream-property [:schema :logseq.property/type])))
+                                      ;; Assumes :many for :date and :node like infer-property-schema-and-get-property-change
+                                      {:logseq.property/type (:from-type upstream-property) :db/cardinality :many}
+                                      (get-property-schema @(:property-schemas import-state) property-name))
+                                    {:title title :block-uuid block-uuid})))
                                properties-tx)
         converted-property-pages-tx
         (map (fn [kw-name]
@@ -1508,7 +1570,8 @@
 
 (defn- save-from-tx
   "Save importer state from given txs"
-  [txs {:keys [import-state]}]
+  [txs {:keys [import-state] :as _opts}]
+  ;; (when (string/includes? (:file _opts) "some-file.md") (cljs.pprint/pprint txs))
   (when-let [nodes (seq (filter :block/name txs))]
     (swap! (:all-existing-page-uuids import-state) merge (into {} (map (juxt :block/uuid identity) nodes)))))
 
@@ -1527,7 +1590,7 @@
                       :or {notify-user #(println "[WARNING]" (:msg %))
                            log-fn prn}
                       :as *options}]
-  (let [options (assoc *options :notify-user notify-user :log-fn log-fn)
+  (let [options (assoc *options :notify-user notify-user :log-fn log-fn :file file)
         {:keys [pages blocks]} (extract-pages-and-blocks @conn file content options)
         tx-options (merge (build-tx-options options)
                           {:journal-created-ats (build-journal-created-ats pages)})
@@ -1547,7 +1610,7 @@
                                                 (assoc tx-options :whiteboard? (some? (seq whiteboard-pages)))))
                        vec)
         {:keys [property-pages-tx property-page-properties-tx] pages-tx' :pages-tx}
-        (split-pages-and-properties-tx pages-tx old-properties existing-pages (:import-state options))
+        (split-pages-and-properties-tx pages-tx old-properties existing-pages (:import-state options) @(:upstream-properties tx-options))
         ;; _ (when (seq property-pages-tx) (cljs.pprint/pprint {:property-pages-tx property-pages-tx}))
         ;; Necessary to transact new property entities first so that block+page properties can be transacted next
         main-props-tx-report (d/transact! conn property-pages-tx {::new-graph? true ::path file})

+ 28 - 4
deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs

@@ -206,7 +206,7 @@
 
       ;; Counts
       ;; Includes journals as property values e.g. :logseq.property/deadline
-      (is (= 26 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Journal]] @conn))))
+      (is (= 27 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Journal]] @conn))))
 
       (is (= 3 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Asset]] @conn))))
       (is (= 4 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Task]] @conn))))
@@ -253,7 +253,7 @@
               set))))
 
     (testing "user properties"
-      (is (= 19
+      (is (= 20
              (->> @conn
                   (d/q '[:find [(pull ?b [:db/ident]) ...]
                          :where [?b :block/tags :logseq.class/Property]])
@@ -396,10 +396,10 @@
               :logseq.property/query "{:query (task todo doing)}"
               :block/tags [:logseq.class/Query]
               :logseq.property.table/ordered-columns [:block/title]}
-             (db-test/readable-properties (db-test/find-block-by-content @conn #"tasks with")))
+             (db-test/readable-properties (db-test/find-block-by-content @conn #"tasks with todo")))
           "Advanced query has correct query properties")
       (is (= "tasks with todo and doing"
-             (:block/title (db-test/find-block-by-content @conn #"tasks with")))
+             (:block/title (db-test/find-block-by-content @conn #"tasks with todo")))
           "Advanced query has custom title migrated")
 
       ;; Cards
@@ -429,6 +429,26 @@
              (:block/title (db-test/find-block-by-content @conn #"Learn Datalog")))
           "Imports full quote with various ast types"))
 
+    (testing "embeds"
+      (is (= {:block/title ""}
+             (-> (d/q '[:find [(pull ?b [*]) ...]
+                        :in $ ?title
+                        :where [?b :block/link ?l] [?b :block/page ?bp] [?bp :block/journal-day 20250612] [?l :block/title ?title]]
+                      @conn
+                      "page embed")
+                 first
+                 (select-keys [:block/title])))
+          "Page embed linked correctly")
+      (is (= {:block/title ""}
+             (-> (d/q '[:find [(pull ?b [*]) ...]
+                        :in $ ?title
+                        :where [?b :block/link ?l] [?b :block/page ?bp] [?bp :block/journal-day 20250612] [?l :block/title ?title]]
+                      @conn
+                      "test block embed")
+                 first
+                 (select-keys [:block/title])))
+          "Block embed linked correctly"))
+
     (testing "tags convert to classes"
       (is (= :user.class/Quotes___life
              (:db/ident (db-test/find-page-by-title @conn "life")))
@@ -503,7 +523,11 @@
         (is (= "20"
                (:user.property/duration (db-test/readable-properties (db-test/find-block-by-content @conn "existing :number to :default"))))
             "existing :number property value correctly saved as :default")
+        (is (= :default
+               (:logseq.property/type (d/entity @conn :user.property/people2)))
+            ":node property changes to :default when :node is defined in same file")
 
+        ;; tests :node :many to :default transition after :node is defined in separate file
         (is (= {:logseq.property/type :default :db/cardinality :db.cardinality/many}
                (select-keys (d/entity @conn :user.property/people) [:logseq.property/type :db/cardinality]))
             ":node property to :default value changes to :default and keeps existing cardinality")

+ 16 - 1
deps/graph-parser/test/resources/exporter-test-graph/journals/2024_08_07.md

@@ -41,4 +41,19 @@
     :breadcrumb-show? true
     :collapsed? False
   }
-  #+END_QUERY
+  #+END_QUERY
+- Get all tasks with a tag "project"
+  #+BEGIN_SRC clojure
+  #+BEGIN_QUERY
+  {:title "All blocks with tag project"
+    :query [:find (pull ?b [*])
+            :where
+            [?p :block/name "project"]
+            [?b :block/refs ?p]]}
+  #+END_QUERY
+  #+END_SRC
+- Find the blocks containing `tag2` but not `tag1`
+
+  #+BEGIN_EXAMPLE
+  {{query (and [[tag2]] (not [[tag1]]))}}
+  #+END_EXAMPLE 

+ 6 - 1
deps/graph-parser/test/resources/exporter-test-graph/journals/2025_06_12.md

@@ -16,4 +16,9 @@
   #+BEGIN_QUOTE
   *Italic* ~~Strikethrough~~ ^^Highlight^^ #[[foo]]
   **Learn Datalog Today** is an interactive tutorial designed to teach you the [Datomic](http://datomic.com/) dialect of [Datalog](http://en.wikipedia.org/wiki/Datalog). Datalog is a declarative **database query language** with roots in logic programming. Datalog has similar expressive power as [SQL](http://en.wikipedia.org/wiki/Sql).
-  #+END_QUOTE
+  #+END_QUOTE
+- test page embed
+  {{embed [[page embed]]}}
+- test block embed
+  id:: 685434e1-0bb9-468c-a660-1642b00b2854
+- {{embed ((685434e1-0bb9-468c-a660-1642b00b2854))}}

+ 4 - 0
deps/graph-parser/test/resources/exporter-test-graph/journals/2025_06_23.md

@@ -0,0 +1,4 @@
+- tests :node to :default like :people property but for a new property
+  people2:: [[Gabriel]]
+- changes :node to :default
+  people2:: some text

+ 27 - 47
deps/outliner/src/logseq/outliner/validate.cljs

@@ -48,26 +48,8 @@
                      :payload {:message (or message "Built-in pages can't be edited")
                                :type :warning}}))))
 
-(defn- validate-unique-by-extends-and-name [db entity new-title]
-  (when-let [_res (seq (d/q '[:find [?b ...]
-                              :in $ ?eid ?type ?title
-                              :where
-                              [?b :block/title ?title]
-                              [?b :logseq.property.class/extends ?type]
-                              [(not= ?b ?eid)]]
-                            db
-                            (:db/id entity)
-                            (:db/id (:logseq.property.class/extends entity))
-                            new-title))]
-    (throw (ex-info "Duplicate page by parent"
-                    {:type :notification
-                     :payload {:message (str "Another page named " (pr-str new-title) " already exists for parents "
-                                             (pr-str (->> (ldb/get-class-extends entity)
-                                                          (map :block/title)
-                                                          (string/join ns-util/parent-char))))
-                               :type :warning}}))))
-
-(defn- another-id-q
+(defn- find-other-ids-with-title-and-tags
+  "Query that finds other ids given the id to ignore, title to look up and tags to consider"
   [entity]
   (cond
     (ldb/property? entity)
@@ -80,17 +62,6 @@
       [?b :block/tags ?tag-id]
       [(missing? $ ?b :logseq.property/built-in?)]
       [(not= ?b ?eid)]]
-    (:logseq.property.class/extends entity)
-    '[:find [?b ...]
-      :in $ ?eid ?title [?tag-id ...]
-      :where
-      [?b :block/title ?title]
-      [?b :block/tags ?tag-id]
-      [(not= ?b ?eid)]
-      ;; same extends
-      [?b :logseq.property.class/extends ?bp]
-      [?eid :logseq.property.class/extends ?ep]
-      [(= ?bp ?ep)]]
     (:block/parent entity)
     '[:find [?b ...]
       :in $ ?eid ?title [?tag-id ...]
@@ -112,10 +83,9 @@
 
 (defn- validate-unique-for-page
   [db new-title {:block/keys [tags] :as entity}]
-  (cond
-    (seq tags)
+  (when (seq tags)
     (when-let [another-id (first
-                           (d/q (another-id-q entity)
+                           (d/q (find-other-ids-with-title-and-tags entity)
                                 db
                                 (:db/id entity)
                                 new-title
@@ -127,20 +97,30 @@
         (when-not (and (= common-tag-ids #{:logseq.class/Page})
                        (> (count this-tags) 1)
                        (> (count another-tags) 1))
-          (throw (ex-info "Duplicate page"
-                          {:type :notification
-                           :payload {:message (str "Another page named " (pr-str new-title) " already exists for tags: "
-                                                   (string/join ", "
-                                                                (map (fn [id] (str "#" (:block/title (d/entity db id)))) common-tag-ids)))
-                                     :type :warning}})))))
-
-    (:logseq.property.class/extends entity)
-    (validate-unique-by-extends-and-name db entity new-title)))
-
-(defn ^:api validate-unique-by-name-tag-and-block-type
+          (cond
+            (ldb/property? entity)
+            (throw (ex-info "Duplicate property"
+                            {:type :notification
+                             :payload {:message (str "Another property named " (pr-str new-title) " already exists.")
+                                       :type :warning}}))
+            (ldb/class? entity)
+            (throw (ex-info "Duplicate class"
+                            {:type :notification
+                             :payload {:message (str "Another tag named " (pr-str new-title) " already exists.")
+                                       :type :warning}}))
+            :else
+            (throw (ex-info "Duplicate page"
+                            {:type :notification
+                             :payload {:message (str "Another page named " (pr-str new-title) " already exists for tags: "
+                                                     (string/join ", "
+                                                                  (map (fn [id] (str "#" (:block/title (d/entity db id)))) common-tag-ids)))
+                                       :type :warning}}))))))))
+
+(defn ^:api validate-unique-by-name-and-tags
   "Validates uniqueness of nodes for the following cases:
    - Page names are unique for a tag e.g. their can be Apple #Company and Apple #Fruit
-   - Page names are unique for a :logseq.property.class/extends"
+   - Property names are unique with user properties being allowed to have the same name as built-in ones
+   - Class names are unique regardless of their extends or if they're built-in"
   [db new-title entity]
   (when (entity-util/page? entity)
     (validate-unique-for-page db new-title entity)))
@@ -159,7 +139,7 @@
   "Validates a block title when it has changed for a entity-util/page? or tagged node"
   [db new-title existing-block-entity]
   (validate-built-in-pages existing-block-entity)
-  (validate-unique-by-name-tag-and-block-type db new-title existing-block-entity)
+  (validate-unique-by-name-and-tags db new-title existing-block-entity)
   (validate-disallow-page-with-journal-name new-title existing-block-entity))
 
 (defn validate-property-title

+ 30 - 8
deps/outliner/test/logseq/outliner/validate_test.cljs

@@ -11,7 +11,7 @@
                             :color2 {:logseq.property/type :default}}})]
 
     (is (nil?
-         (outliner-validate/validate-unique-by-name-tag-and-block-type
+         (outliner-validate/validate-unique-by-name-and-tags
           @conn
           (:block/title (d/entity @conn :logseq.property/background-color))
           (d/entity @conn :user.property/color)))
@@ -19,13 +19,35 @@
 
     (is (thrown-with-msg?
          js/Error
-         #"Duplicate page"
-         (outliner-validate/validate-unique-by-name-tag-and-block-type
+         #"Duplicate property"
+         (outliner-validate/validate-unique-by-name-and-tags
           @conn
           "color"
           (d/entity @conn :user.property/color2)))
         "Disallow duplicate user property")))
 
+(deftest validate-block-title-unique-for-tags
+  (let [conn (db-test/create-conn-with-blocks
+              {:classes {:Class1 {}
+                         :Class2 {:logseq.property.class/extends :logseq.class/Task}}})]
+
+    (is (thrown-with-msg?
+         js/Error
+         #"Duplicate class"
+         (outliner-validate/validate-unique-by-name-and-tags
+          @conn
+          "Class1"
+          (d/entity @conn :user.class/Class2)))
+        "Disallow duplicate class names, regardless of extends")
+    (is (thrown-with-msg?
+         js/Error
+         #"Duplicate class"
+         (outliner-validate/validate-unique-by-name-and-tags
+          @conn
+          "Card"
+          (d/entity @conn :user.class/Class1)))
+        "Disallow duplicate class names even if it's built-in")))
+
 (deftest validate-block-title-unique-for-pages
   (let [conn (db-test/create-conn-with-blocks
               [{:page {:block/title "page1"}}
@@ -37,13 +59,13 @@
     (is (thrown-with-msg?
          js/Error
          #"Duplicate page"
-         (outliner-validate/validate-unique-by-name-tag-and-block-type
+         (outliner-validate/validate-unique-by-name-and-tags
           @conn
           "Apple"
           (db-test/find-page-by-title @conn "Another Company")))
         "Disallow duplicate page with tag")
     (is (nil?
-         (outliner-validate/validate-unique-by-name-tag-and-block-type
+         (outliner-validate/validate-unique-by-name-and-tags
           @conn
           "Apple"
           (db-test/find-page-by-title @conn "Banana")))
@@ -52,14 +74,14 @@
     (is (thrown-with-msg?
          js/Error
          #"Duplicate page"
-         (outliner-validate/validate-unique-by-name-tag-and-block-type
+         (outliner-validate/validate-unique-by-name-and-tags
           @conn
           "page1"
           (db-test/find-page-by-title @conn "another page")))
         "Disallow duplicate page without tag")
 
     (is (nil?
-         (outliner-validate/validate-unique-by-name-tag-and-block-type
+         (outliner-validate/validate-unique-by-name-and-tags
           @conn
           "Apple"
           (db-test/find-page-by-title @conn "Fruit")))
@@ -151,7 +173,7 @@
             page-errors (atom {})]
         (doseq [page pages]
           (try
-            (outliner-validate/validate-unique-by-name-tag-and-block-type @conn (:block/title page) page)
+            (outliner-validate/validate-unique-by-name-and-tags @conn (:block/title page) page)
             (outliner-validate/validate-page-title (:block/title page) {:node page})
             (outliner-validate/validate-page-title-characters (:block/title page) {:node page})
 

+ 2 - 0
public/index.html

@@ -53,6 +53,8 @@
 <script defer type="module" src="/static/js/pdfjs/pdf.mjs"></script>
 <script defer type="module" src="/static/js/pdf_viewer3.mjs"></script>
 <script defer src="/static/js/eventemitter3.umd.min.js"></script>
+<script defer src="/static/js/photoswipe.umd.min.js"></script>
+<script defer src="/static/js/photoswipe-lightbox.umd.min.js"></script>
 <script defer src="/static/js/html2canvas.min.js"></script>
 <script defer src="/static/js/lsplugin.core.js"></script>
 <script defer src="/static/js/react.production.min.js"></script>

+ 2 - 0
resources/index.html

@@ -52,6 +52,8 @@ const portal = new MagicPortal(worker);
 <script defer type="module" src="./js/pdfjs/pdf.mjs"></script>
 <script defer type="module" src="./js/pdf_viewer3.mjs"></script>
 <script defer src="./js/eventemitter3.umd.min.js"></script>
+<script defer src="./js/photoswipe.umd.min.js"></script>
+<script defer src="./js/photoswipe-lightbox.umd.min.js"></script>
 <script defer src="./js/html2canvas.min.js"></script>
 <script defer src="./js/lsplugin.core.js"></script>
 <script defer src="./js/react.production.min.js"></script>

+ 121 - 110
src/main/frontend/components/block.cljs

@@ -256,7 +256,7 @@
           [:p.text-error.text-xs [:small.opacity-80
                                   (util/format "%s not found!" (string/capitalize type))]])))))
 
-(defn open-lightbox
+(defn open-lightbox!
   [e]
   (let [images (js/document.querySelectorAll ".asset-container img")
         images (to-array images)
@@ -307,83 +307,95 @@
 (defonce *resizing-image? (atom false))
 (rum/defc asset-container
   [asset-block src title metadata {:keys [breadcrumb? positioned? local? full-text]}]
-  [:div.asset-container
-   {:key "resize-asset-container"}
-   [:img.rounded-sm.relative
-    (merge
-     {:loading "lazy"
-      :referrerPolicy "no-referrer"
-      :src src
-      :title title}
-     metadata)]
-   (when (and (not breadcrumb?)
-              (not positioned?))
-     [:<>
-      (let [image-src (fs/asset-path-normalize src)]
-        [:.asset-action-bar {:aria-hidden "true"}
-         [:.flex
-          (when-not config/publishing?
-            [:button.asset-action-btn
-             {:title (t :asset/delete)
-              :tabIndex "-1"
-              :on-pointer-down util/stop
-              :on-click
-              (fn [e]
-                (util/stop e)
-                (when-let [block-id (some-> (.-target e) (.closest "[blockid]") (.getAttribute "blockid") (uuid))]
+  (let [*el-ref (rum/use-ref nil)
+        image-src (fs/asset-path-normalize src)
+        get-blockid #(some-> (rum/deref *el-ref) (.closest "[blockid]") (.getAttribute "blockid") (uuid))]
+    [:div.asset-container
+     {:key "resize-asset-container"
+      :on-pointer-down util/stop
+      :on-click (fn [e]
+                  (when (= "IMG" (some-> (.-target e) (.-nodeName)))
+                    (open-lightbox! e)))
+      :ref *el-ref}
+     [:img.rounded-sm.relative
+      (merge
+        {:loading "lazy"
+         :referrerPolicy "no-referrer"
+         :src src
+         :title title}
+        metadata)]
+     (when (and (not breadcrumb?)
+             (not positioned?))
+       [:<>
+        (let [handle-copy!
+              (fn [_e]
+                (-> (util/copy-image-to-clipboard image-src)
+                  (p/then #(notification/show! "Copied!" :success))))
+              handle-delete!
+              (fn [_e]
+                (when-let [block-id (get-blockid)]
                   (let [*local-selected? (atom local?)]
                     (-> (shui/dialog-confirm!
-                         [:div.text-xs.opacity-60.-my-2
-                          (when (and local? (not= (:block/uuid asset-block) block-id))
-                            [:label.flex.gap-1.items-center
-                             (shui/checkbox
-                              {:default-checked @*local-selected?
-                               :on-checked-change #(reset! *local-selected? %)})
-                             (t :asset/physical-delete)])]
-                         {:title (t :asset/confirm-delete (.toLocaleLowerCase (t :text/image)))
-                          :outside-cancel? true})
-                        (p/then (fn []
-                                  (shui/dialog-close!)
-                                  (editor-handler/delete-asset-of-block!
-                                   {:block-id block-id
-                                    :asset-block asset-block
-                                    :local? local?
-                                    :delete-local? @*local-selected?
-                                    :repo (state/get-current-repo)
-                                    :href src
-                                    :title title
-                                    :full-text full-text})))))))}
-             (ui/icon "trash")])
-
-          [:button.asset-action-btn
-           {:title (t :asset/copy)
-            :tabIndex "-1"
-            :on-pointer-down util/stop
-            :on-click (fn [e]
-                        (util/stop e)
-                        (-> (util/copy-image-to-clipboard image-src)
-                            (p/then #(notification/show! "Copied!" :success))))}
-           (ui/icon "copy")]
-
-          [:button.asset-action-btn
-           {:title (t :asset/maximize)
-            :tabIndex "-1"
-            :on-pointer-down util/stop
-            :on-click open-lightbox}
-
-           (ui/icon "maximize")]
-
-          (when (util/electron?)
-            [:button.asset-action-btn
-             {:title (t (if local? :asset/show-in-folder :asset/open-in-browser))
-              :tabIndex "-1"
-              :on-pointer-down util/stop
-              :on-click (fn [e]
-                          (util/stop e)
-                          (if local?
-                            (ipc/ipc "openFileInFolder" image-src)
-                            (js/window.apis.openExternal image-src)))}
-             (shui/tabler-icon "folder-pin")])]])])])
+                          [:div.text-xs.opacity-60.-my-2
+                           (when (and local? (not= (:block/uuid asset-block) block-id))
+                             [:label.flex.gap-1.items-center
+                              (shui/checkbox
+                                {:default-checked @*local-selected?
+                                 :on-checked-change #(reset! *local-selected? %)})
+                              (t :asset/physical-delete)])]
+                          {:title (t :asset/confirm-delete (.toLocaleLowerCase (t :text/image)))
+                           :outside-cancel? true})
+                      (p/then (fn []
+                                (shui/dialog-close!)
+                                (editor-handler/delete-asset-of-block!
+                                  {:block-id block-id
+                                   :asset-block asset-block
+                                   :local? local?
+                                   :delete-local? @*local-selected?
+                                   :repo (state/get-current-repo)
+                                   :href src
+                                   :title title
+                                   :full-text full-text})))))))]
+          [:.asset-action-bar {:aria-hidden "true"}
+           (shui/button-group
+             (shui/button
+               {:variant :outline
+                :size :icon
+                :class "h-7 w-7"
+                :on-pointer-down util/stop
+                :on-click (fn [e]
+                            (shui/popup-show! (.closest (.-target e) ".asset-action-bar")
+                              (fn []
+                                [:div
+                                 {:on-click #(shui/popup-hide!)}
+                                 (shui/dropdown-menu-item
+                                   {:on-click #(some-> (db/entity [:block/uuid (get-blockid)]) (editor-handler/edit-block! :max))}
+                                   [:span.flex.items-center.gap-1
+                                    (ui/icon "edit") (t :asset/edit-block)])
+                                 (shui/dropdown-menu-item
+                                   {:on-click handle-copy!}
+                                   [:span.flex.items-center.gap-1
+                                    (ui/icon "copy") (t :asset/copy)])
+                                 (when (util/electron?)
+                                   (shui/dropdown-menu-item
+                                     {:on-click (fn [e]
+                                                  (util/stop e)
+                                                  (if local?
+                                                    (ipc/ipc "openFileInFolder" image-src)
+                                                    (js/window.apis.openExternal image-src)))}
+                                     [:span.flex.items-center.gap-1
+                                      (ui/icon "folder-pin") (t (if local? :asset/show-in-folder :asset/open-in-browser))]))
+
+                                 (when-not config/publishing?
+                                   [:<>
+                                    (shui/dropdown-menu-separator)
+                                    (shui/dropdown-menu-item
+                                      {:on-click handle-delete!}
+                                      [:span.flex.items-center.gap-1.text-red-700
+                                       (ui/icon "trash") (t :asset/delete)])])
+                                 ])
+                              {:align :start}))}
+               (shui/tabler-icon "dots-vertical")))])])]))
 
 ;; TODO: store image height and width for better ux
 (rum/defcs ^:large-vars/cleanup-todo resizable-image <
@@ -396,32 +408,32 @@
         positioned? (:property-position config)
         asset-block (:asset-block config)
         width (or (get-in asset-block [:logseq.property.asset/resize-metadata :width])
-                  (:width metadata))
+                (:width metadata))
         *width (get state ::size)
         width (or @*width width 250)
         metadata' (merge
-                   (cond->
-                    {:height 125}
-                     width
-                     (assoc :width width))
-                   metadata)
+                    (cond->
+                      {:height 125}
+                      width
+                      (assoc :width width))
+                    metadata)
         resizable? (and (not (mobile-util/native-platform?))
-                        (not breadcrumb?)
-                        (not positioned?))
+                     (not breadcrumb?)
+                     (not positioned?))
         asset-container-cp (asset-container asset-block src title metadata'
-                                            {:breadcrumb? breadcrumb?
-                                             :positioned? positioned?
-                                             :local? local?
-                                             :full-text full-text})]
+                             {:breadcrumb? breadcrumb?
+                              :positioned? positioned?
+                              :local? local?
+                              :full-text full-text})]
     (if (or (:disable-resize? config)
-            (not resizable?))
+          (not resizable?))
       asset-container-cp
       [:div.ls-resize-image.rounded-md
        asset-container-cp
        (resize-image-handles
-        (fn [k ^js event]
-          (let [dx (.-dx event)
-                ^js target (.-target event)]
+         (fn [k ^js event]
+           (let [dx (.-dx event)
+                 ^js target (.-target event)]
 
             (case k
               :start
@@ -689,10 +701,9 @@
 
    All page-names are sanitized except page-name-in-block"
   [state
-   {:keys [contents-page? whiteboard-page? other-position? show-unique-title? stop-click-event?
+   {:keys [contents-page? whiteboard-page? other-position? show-unique-title?
            on-context-menu with-parent?]
-    :or {stop-click-event? true
-         with-parent? true}
+    :or {with-parent? true}
     :as config}
    page-entity children label]
   (let [*hover? (::hover? state)
@@ -718,9 +729,6 @@
                         (editor-handler/block->data-transfer! page-name e true))
        :on-mouse-over #(reset! *hover? true)
        :on-mouse-leave #(reset! *hover? false)
-       :on-click (fn [e]
-                   (when (and stop-click-event? (not (util/link? (.-target e))))
-                     (util/stop e)))
        :on-pointer-down (fn [^js e]
                           (cond
                             (util/link? (.-target e))
@@ -741,7 +749,6 @@
                               (reset! *mouse-down? true))))
        :on-pointer-up (fn [e]
                         (when @*mouse-down?
-                          (util/stop e)
                           (state/clear-edit!)
                           (when-not (:disable-click? config)
                             (<open-page-ref config page-entity e page-name contents-page?))
@@ -1550,9 +1557,10 @@
       (->elem
        :a
        (cond->
-        {:href (path/path-join "file://" path)
-         :data-href path
-         :target    "_blank"}
+        {:on-click (fn [e]
+                     (util/stop e)
+                     (js/window.apis.openPath path))
+         :data-href path}
          title
          (assoc :title title))
        (map-inline config label)))
@@ -1632,9 +1640,14 @@
                               href)]
                   (->elem
                    :a
-                   (cond-> {:href      (path/path-join "file://" href*)
-                            :data-href href*
-                            :target    "_blank"}
+                   (cond-> (if (util/electron?)
+                             {:on-click (fn [e]
+                                          (util/stop e)
+                                          (js/window.apis.openPath path))
+                              :data-href href*}
+                             {:href      (path/path-join "file://" href*)
+                              :data-href href*
+                              :target    "_blank"})
                      title (assoc :title title))
                    (map-inline config label))))))
 
@@ -1903,7 +1916,7 @@
 
       (= name "namespace")
       (if (config/db-based-graph? (state/get-current-repo))
-        [:div.warning "Namespace is deprecated, use tags instead"]
+        [:div.warning (str "{{namespace}} is deprecated. Use the " common-config/library-page-name " feature instead.")]
         (let [namespace (first arguments)]
           (when-not (string/blank? namespace)
             (let [namespace (string/lower-case (page-ref/get-page-name! namespace))
@@ -2856,8 +2869,7 @@
                                                          (ui/icon "X" {:size 14})))
                                                       (page-cp (assoc config
                                                                       :tag? true
-                                                                      :disable-preview? true
-                                                                      :stop-click-event? false) tag)]))
+                                                                      :disable-preview? true) tag)]))
                                                  popup-opts))}
            (for [tag (take 2 block-tags)]
              [:div.block-tag.pl-2
@@ -3261,8 +3273,7 @@
                   rest)
         config (assoc config
                       :breadcrumb? true
-                      :disable-preview? true
-                      :stop-click-event? false)]
+                      :disable-preview? true)]
     (when (seq parents)
       (let [parents-props (doall
                            (for [{:block/keys [uuid name title] :as block} parents]

+ 12 - 3
src/main/frontend/components/block.css

@@ -29,8 +29,11 @@
     @apply relative inline-block mt-2 w-full;
 
     .asset-action-bar {
-      @apply top-0.5 right-0.5 absolute flex items-center
-      border bg-gray-02 rounded opacity-0 transition-opacity;
+      @apply top-1 right-1 absolute flex items-center opacity-0 transition-opacity;
+
+      &[data-popup-active] {
+        @apply opacity-100;
+      }
     }
 
     .asset-action-btn {
@@ -117,6 +120,12 @@
   }
 }
 
+.breadcrumb {
+  .property-value-inner[data-type], .property-value-inner .select-item {
+    display: inline;
+  }
+}
+
 .open-block-ref-link {
   background-color: var(--ls-page-properties-background-color);
   padding: 1px 4px;
@@ -1118,7 +1127,7 @@ html.is-mac {
 }
 
 .ls-resize-image {
-  @apply flex relative;
+  @apply flex relative w-fit cursor-pointer;
 
   .handle-left, .handle-right {
     @apply absolute w-[6px] h-[15%] min-h-[30px] bg-black/30 hover:bg-black/70

+ 14 - 9
src/main/frontend/components/editor.cljs

@@ -136,18 +136,20 @@
 
 (defn- matched-pages-with-new-page [partial-matched-pages db-tag? q]
   (if (or
-       (if db-tag?
-         (let [entity (db/get-page q)]
-           (and (ldb/internal-page? entity) (= (:block/title entity) q)))
-         ;; Page existence here should be the same as entity-util/page?.
-         ;; Don't show 'New page' if a page has any of these tags
-         (db/page-exists? q db-class/page-classes))
-
+       (db/page-exists? q (if db-tag?
+                            #{:logseq.class/Tag}
+                            ;; Page existence here should be the same as entity-util/page?.
+                            ;; Don't show 'New page' if a page has any of these tags
+                            db-class/page-classes))
        (and db-tag? (some ldb/class? (:block/_alias (db/get-page q)))))
     partial-matched-pages
     (if db-tag?
-      (concat [{:block/title (str (t :new-tag) " " q)}]
-              partial-matched-pages)
+      (concat
+       ;; Don't show 'New tag' for an internal page because it already shows 'Convert ...'
+       (when-not (let [entity (db/get-page q)]
+                   (and (ldb/internal-page? entity) (= (:block/title entity) q)))
+         [{:block/title (str (t :new-tag) " " q)}])
+       partial-matched-pages)
       (cons {:block/title (str (t :new-page) " " q)}
             partial-matched-pages))))
 
@@ -757,6 +759,9 @@
       (and (= type :esc) (editor-handler/editor-commands-popup-exists?))
       nil
 
+      (state/editor-in-composition?)
+      nil
+
       (or (contains?
            #{:commands :page-search :page-search-hashtag :block-search :template-search
              :property-search :property-value-search :datepicker}

+ 20 - 12
src/main/frontend/components/property/value.cljs

@@ -393,7 +393,7 @@
 
 (rum/defc overdue
   [date content]
-  (let [[current-time set-current-time!] (rum/use-state (t/now))]
+  (let [[current-time set-current-time!] (hooks/use-state (t/now))]
     (hooks/use-effect!
      (fn []
        (let [timer (js/setInterval (fn [] (set-current-time! (t/now))) (* 1000 60 3))]
@@ -455,7 +455,7 @@
 
 (rum/defc date-picker
   [value {:keys [block property datetime? on-change on-delete del-btn? editing? multiple-values? other-position?]}]
-  (let [*el (rum/use-ref nil)
+  (let [*el (hooks/use-ref nil)
         content-fn (fn [{:keys [id]}] (calendar-inner id
                                                       {:block block
                                                        :property property
@@ -836,8 +836,8 @@
 (rum/defc property-value-select-node < rum/static
   [block property opts
    {:keys [*show-new-property-config?]}]
-  (let [[initial-choices set-initial-choices!] (rum/use-state nil)
-        [result set-result!] (rum/use-state nil)
+  (let [[initial-choices set-initial-choices!] (hooks/use-state nil)
+        [result set-result!] (hooks/use-state nil)
         set-result-and-initial-choices! (fn [value]
                                           (set-initial-choices! value)
                                           (set-result! value))
@@ -1144,7 +1144,7 @@
 
 (rum/defc single-value-select
   [block property value select-opts {:keys [value-render] :as opts}]
-  (let [*el (rum/use-ref nil)
+  (let [*el (hooks/use-ref nil)
         editing? (:editing? opts)
         type (:logseq.property/type property)
         select-opts' (assoc select-opts :multiple-choices? false)
@@ -1227,11 +1227,12 @@
 
 (rum/defc single-number-input
   [block property value-block table-view?]
-  (let [[editing? set-editing!] (rum/use-state false)
-        *ref (rum/use-ref nil)
-        *input-ref (rum/use-ref nil)
+  (let [[editing? set-editing!] (hooks/use-state false)
+        *ref (hooks/use-ref nil)
+        *input-ref (hooks/use-ref nil)
         number-value (db-property/property-value-content value-block)
-        [value set-value!] (rum/use-state number-value)
+        [value set-value!] (hooks/use-state number-value)
+        [*value _] (hooks/use-state (atom value))
         set-property-value! (fn [value & {:keys [exit-editing?]
                                           :or {exit-editing? true}}]
                               (p/do!
@@ -1244,6 +1245,10 @@
 
                                (when exit-editing?
                                  (set-editing! false))))]
+    (hooks/use-effect!
+     (fn []
+       #(set-property-value! @*value))
+     [])
     [:div.ls-number.flex.flex-1.jtrigger
      {:ref *ref
       :on-click #(do
@@ -1256,8 +1261,11 @@
          :class (str "ls-number-input h-6 px-0 py-0 border-none bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 text-base"
                      (when table-view? " text-sm"))
          :value value
-         :on-change (fn [e] (set-value! (util/evalue e)))
-         :on-blur (fn [_e] (set-property-value! value))
+         :on-change (fn [e]
+                      (set-value! (util/evalue e))
+                      (reset! *value (util/evalue e)))
+         :on-blur (fn [_e]
+                    (set-property-value! value))
          :on-key-down (fn [e]
                         (let [input (rum/deref *input-ref)
                               pos (cursor/pos input)
@@ -1369,7 +1377,7 @@
   [block property v {:keys [on-chosen editing?] :as opts}]
   (let [type (:logseq.property/type property)
         date? (= type :date)
-        *el (rum/use-ref nil)
+        *el (hooks/use-ref nil)
         items (cond->> (if (entity-map? v) #{v} v)
                 (= (:db/ident property) :block/tags)
                 (remove (fn [v] (contains? ldb/hidden-tags (:db/ident v)))))

+ 4 - 2
src/main/frontend/components/repo.cljs

@@ -102,13 +102,14 @@
            {:asChild true}
            (shui/button
             {:variant "ghost"
-             :class "!px-1"
+             :class "graph-action-btn !px-1"
              :size :sm}
             (ui/icon "dots" {:size 15})))
           (shui/dropdown-menu-content
            {:align "end"}
            (shui/dropdown-menu-item
             {:key "delete-locally"
+             :class "delete-local-graph-menu-item"
              :on-click (fn []
                          (let [prompt-str (if db-based?
                                             (str "Are you sure to permanently delete the graph \"" graph-name "\" from Logseq?")
@@ -122,10 +123,11 @@
                                (p/then (fn []
                                          (repo-handler/remove-repo! repo)
                                          (state/pub-event! [:graph/unlinked repo (state/get-current-repo)]))))))}
-            "Delete")
+            "Delete local graph")
            (when (and remote? (or (and db-based? manager?) (not db-based?)))
              (shui/dropdown-menu-item
               {:key "delete-remotely"
+               :class "delete-remote-graph-menu-item"
                :on-click (fn []
                            (let [prompt-str (str "Are you sure to permanently delete the graph \"" graph-name "\" from our server?")]
                              (-> (shui/dialog-confirm!

+ 6 - 10
src/main/frontend/extensions/lightbox.cljs

@@ -1,14 +1,10 @@
 (ns frontend.extensions.lightbox
-  (:require [promesa.core :as p]
-            [cljs-bean.core :as bean]
-            [frontend.util :as util]))
+  (:require [cljs-bean.core :as bean]))
 
 (defn preview-images!
   [images]
-  (p/let [_ (util/js-load$ (str util/JS_ROOT "/photoswipe.umd.min.js"))
-          _ (util/js-load$ (str util/JS_ROOT "/photoswipe-lightbox.umd.min.js"))]
-    (let [options {:dataSource images :pswpModule js/window.PhotoSwipe :showHideAnimationType "fade"}
-          ^js lightbox (js/window.PhotoSwipeLightbox. (bean/->js options))]
-      (doto lightbox
-        (.init)
-        (.loadAndOpen 0)))))
+  (let [options {:dataSource images :pswpModule js/window.PhotoSwipe :showHideAnimationType "fade"}
+        ^js lightbox (js/window.PhotoSwipeLightbox. (bean/->js options))]
+    (doto lightbox
+      (.init)
+      (.loadAndOpen 0))))

+ 2 - 2
src/main/frontend/extensions/pdf/assets.cljs

@@ -380,7 +380,7 @@
        (when-let [e (some->> (:key current) (str "hls__") (db-model/get-page))]
          (rfe/push-state :page {:name (str (:block/uuid e))} (if id {:anchor (str "block-content-" + id)} nil)))))))
 
-(defn open-lightbox
+(defn open-lightbox!
   [e]
   (let [images (js/document.querySelectorAll ".hl-area img")
         images (to-array images)
@@ -443,7 +443,7 @@
               {:title (t :asset/maximize)
                :tabIndex "-1"
                :on-pointer-down util/stop
-               :on-click open-lightbox}
+               :on-click open-lightbox!}
 
               (ui/icon "maximize")]]
             [:img.w-full {:src @*src}]]])))))

+ 1 - 1
src/main/frontend/handler/db_based/page.cljs

@@ -23,7 +23,7 @@
    When returning false, this fn also displays appropriate notifications to the user"
   [repo block tag-entity]
   (try
-    (outliner-validate/validate-unique-by-name-tag-and-block-type
+    (outliner-validate/validate-unique-by-name-and-tags
      (db/get-db repo)
      (:block/title block)
      (update block :block/tags (fnil conj #{}) tag-entity))

+ 4 - 1
src/main/frontend/handler/editor.cljs

@@ -2706,7 +2706,10 @@
       (let [repo (state/get-current-repo)
             editor-state (assoc (get-state)
                                 :block-id (:block/uuid next-block)
-                                :value (:block/title next-block))]
+                                :value (:block/title next-block)
+                                :block-container (util/get-next-block-non-collapsed
+                                                  (util/rec-get-node (state/get-input) "ls-block")
+                                                  {:exclude-property? true}))]
         (delete-block-inner! repo editor-state)))))
 
 (defn keydown-delete-handler

+ 25 - 31
src/main/frontend/worker/db/migrate.cljs

@@ -119,12 +119,12 @@
 
 (defn rename-properties
   [props-to-rename & {:keys [replace-fn]}]
-  (fn [conn]
-    (when (ldb/db-based-graph? @conn)
-      (let [props-tx (rename-properties-aux @conn props-to-rename)
+  (fn [db]
+    (when (ldb/db-based-graph? db)
+      (let [props-tx (rename-properties-aux db props-to-rename)
             fix-tx (mapcat (fn [[old new]]
                              ;; can't use datoms b/c user properties aren't indexed
-                             (->> (d/q '[:find ?b ?prop-v :in $ ?prop :where [?b ?prop ?prop-v]] @conn old)
+                             (->> (d/q '[:find ?b ?prop-v :in $ ?prop :where [?b ?prop ?prop-v]] db old)
                                   (mapcat (fn [[id prop-value]]
                                             (if (fn? replace-fn)
                                               (replace-fn id prop-value)
@@ -136,10 +136,10 @@
 (comment
   (defn- rename-classes
     [classes-to-rename]
-    (fn [conn _search-db]
-      (when (ldb/db-based-graph? @conn)
+    (fn [db]
+      (when (ldb/db-based-graph? db)
         (mapv (fn [[old new]]
-                (merge {:db/id (:db/id (d/entity @conn old))
+                (merge {:db/id (:db/id (d/entity db old))
                         :db/ident new}
                        (when-let [new-title (get-in db-class/built-in-classes [new :title])]
                          {:block/title new-title
@@ -147,9 +147,8 @@
               classes-to-rename)))))
 
 (defn fix-rename-parent-to-extends
-  [conn _search-db]
-  (let [db @conn
-        parent-entity (d/entity db :logseq.property/parent)]
+  [db]
+  (let [parent-entity (d/entity db :logseq.property/parent)]
     (when parent-entity
       (let [old-p :logseq.property/parent
             new-p :logseq.property.class/extends
@@ -160,7 +159,7 @@
                                     new-p' (if (ldb/class? page) new-p :block/parent)]
                                 [[:db/retract id old-p]
                                  [:db/add id new-p' prop-value]]))})
-            rename-property-tx (f conn)
+            rename-property-tx (f db)
             library-page (if-let [page (ldb/get-built-in-page db common-config/library-page-name)]
                            page
                            (-> (sqlite-util/build-new-page common-config/library-page-name)
@@ -210,11 +209,10 @@
    [:db/retract id :logseq.property/enable-history?]])
 
 (defn separate-classes-and-properties
-  [conn _sqlite-db]
+  [db]
   ;; find all properties that're classes, create new properties to separate them
   ;; from classes.
-  (let [db @conn
-        class-ids (d/q
+  (let [class-ids (d/q
                    '[:find [?b ...]
                      :where
                      [?b :block/tags :logseq.class/Property]
@@ -253,10 +251,9 @@
      class-ids)))
 
 (defn fix-tag-properties
-  [conn _sqlite-db]
+  [db]
   ;; find all classes that're still used as properties
-  (let [db @conn
-        class-ids (d/q
+  (let [class-ids (d/q
                    '[:find [?b ...]
                      :where
                      [?b :block/tags :logseq.class/Tag]
@@ -275,9 +272,8 @@
      class-ids)))
 
 (defn add-missing-db-ident-for-tags
-  [conn _sqlite-db]
-  (let [db @conn
-        class-ids (d/q
+  [db _sqlite-db]
+  (let [class-ids (d/q
                    '[:find [?b ...]
                      :where
                      [?b :block/tags :logseq.class/Tag]
@@ -294,10 +290,9 @@
      class-ids)))
 
 (defn fix-using-properties-as-tags
-  [conn _sqlite-db]
+  [db]
   ;; find all properties that're tags
-  (let [db @conn
-        property-ids (->>
+  (let [property-ids (->>
                       (d/q
                        '[:find ?b ?i
                          :where
@@ -308,7 +303,7 @@
                       (map first))]
     (mapcat
      (fn [id]
-       (let [property (d/entity @conn id)
+       (let [property (d/entity db id)
              title (:block/title property)]
          (into (retract-property-attributes id)
                [[:db/retract id :logseq.property/parent]
@@ -316,10 +311,9 @@
      property-ids)))
 
 (defn remove-block-order-for-tags
-  [conn _sqlite-db]
+  [db]
   ;; find all properties that're tags
-  (let [db @conn
-        tag-ids (d/q
+  (let [tag-ids (d/q
                  '[:find [?b ...]
                    :where
                    [?b :block/tags :logseq.class/Tag]
@@ -408,7 +402,7 @@
                              :db-migrate? true})))
 
 (defn- upgrade-version!
-  [conn search-db db-based? version {:keys [properties classes fix]}]
+  [conn db-based? version {:keys [properties classes fix]}]
   (let [version (db-schema/parse-schema-version version)
         db @conn
         new-properties (->> (select-keys db-property/built-in-properties properties)
@@ -431,7 +425,7 @@
                                  (when-let [db-ident (:db/ident class)]
                                    {:db/ident db-ident})) new-classes)
         fixes (when (fn? fix)
-                (fix conn search-db))
+                (fix db))
         tx-data (if db-based? (concat new-class-idents new-properties new-classes fixes) fixes)
         tx-data' (concat
                   [(sqlite-util/kv :logseq.kv/schema-version version)]
@@ -442,7 +436,7 @@
 (defn migrate
   "Migrate 'frontend' datascript schema and data. To add a new migration,
   add an entry to schema-version->updates and bump db-schema/version"
-  [conn search-db]
+  [conn]
   (when (ldb/db-based-graph? @conn)
     (let [db @conn
           version-in-db (db-schema/parse-schema-version (or (:kv/value (d/entity db :logseq.kv/schema-version)) 0))
@@ -465,7 +459,7 @@
                               schema-version->updates)]
             (println "DB schema migrated from" version-in-db)
             (doseq [[v m] updates]
-              (upgrade-version! conn search-db db-based? v m))
+              (upgrade-version! conn db-based? v m))
             (ensure-built-in-data-exists! conn))
           (catch :default e
             (prn :error (str "DB migration failed to migrate to " db-schema/version " from " version-in-db ":"))

+ 1 - 1
src/main/frontend/worker/db_worker.cljs

@@ -296,7 +296,7 @@
 
         (gc-sqlite-dbs! db client-ops-db conn {})
 
-        (db-migrate/migrate conn search-db)
+        (db-migrate/migrate conn)
 
         (db-listener/listen-db-changes! repo (get @*datascript-conns repo))))))
 

+ 1 - 0
src/resources/dicts/en.edn

@@ -176,6 +176,7 @@
  :asset/copy "Copy image"
  :asset/maximize "Maximize image"
  :asset/ref-block "Asset ref block"
+ :asset/edit-block "Edit block"
  :asset/confirm-delete "Are you sure you want to delete this {1}?"
  :asset/physical-delete "Remove the file too (notice it can't be restored)"
  :color/gray "Gray"

+ 1 - 1
src/test/frontend/test/frontend_node_test_runner.cljs

@@ -5,7 +5,7 @@
             [shadow.test.env :as env]
             [lambdaisland.glogi.console :as glogi-console]
             ;; activate humane test output for all tests
-            [pjstadig.humane-test-output]))
+            #_[pjstadig.humane-test-output]))
 
 ;; Needed for new test runners
 (defn ^:dev/after-load reset-test-data! []