Przeglądaj źródła

Merge branch 'master' into feat/capacitor-new

charlie 7 miesięcy temu
rodzic
commit
08b824934c
27 zmienionych plików z 579 dodań i 418 usunięć
  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]
   (:require [clojure.edn :as edn]
             [clojure.string :as string]
             [clojure.string :as string]
             [logseq.e2e.assert :as assert]
             [logseq.e2e.assert :as assert]
+            [logseq.e2e.locator :as loc]
             [logseq.e2e.util :as util]
             [logseq.e2e.util :as util]
-            [wally.main :as w]
-            [logseq.e2e.locator :as loc]))
+            [wally.main :as w]))
 
 
 (defn- refresh-all-remote-graphs
 (defn- refresh-all-remote-graphs
   []
   []
@@ -39,14 +39,11 @@
 (defn remove-remote-graph
 (defn remove-remote-graph
   [graph-name]
   [graph-name]
   (wait-for-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
 (defn switch-graph
   [to-graph-name wait-sync?]
   [to-graph-name wait-sync?]

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

@@ -8,6 +8,7 @@
 (def enter #(press "Enter"))
 (def enter #(press "Enter"))
 (def esc #(press "Escape"))
 (def esc #(press "Escape"))
 (def backspace #(press "Backspace"))
 (def backspace #(press "Backspace"))
+(def delete #(press "Delete"))
 (def tab #(press "Tab"))
 (def tab #(press "Tab"))
 (def shift+tab #(press "Shift+Tab"))
 (def shift+tab #(press "Shift+Tab"))
 (def shift+enter #(press "Shift+Enter"))
 (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 (= "b1" (util/get-edit-content)))
     (is (= 1 (util/page-blocks-count)))))
     (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 []
 (defn delete-test-with-children []
   (testing "Delete block with its children"
   (testing "Delete block with its children"
     (b/new-blocks ["b1" "b2" "b3" "b4"])
     (b/new-blocks ["b1" "b2" "b3" "b4"])
@@ -88,5 +96,8 @@
 (deftest delete-test
 (deftest delete-test
   (delete))
   (delete))
 
 
+(deftest delete-end-test
+  (delete-end))
+
 (deftest delete-test-with-children-test
 (deftest delete-test-with-children-test
   (delete-test-with-children))
   (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)}]
    [:multi {:dispatch #(-> % first :logseq.property/type)}]
    (map (fn [[prop-type value-schema]]
    (map (fn [[prop-type value-schema]]
           [prop-type
           [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)))
         db-property-type/built-in-validation-schemas)))
 
 
 (def block-properties
 (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
 ;; Validate && list fixes for non-validated values when updating property schema
 
 
 (defn url?
 (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"
    Originally from common-util/url? but does not need to be the same"
   [s]
   [s]
   (and (string? s)
   (and (string? s)
        (try
        (try
-         (not (contains? #{nil "null"} (.-origin (js/URL. s))))
+         (not (contains? #{nil} (.-origin (js/URL. s))))
          (catch :default _e
          (catch :default _e
            false))))
            false))))
 
 

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

@@ -13,6 +13,7 @@
             [logseq.common.config :as common-config]
             [logseq.common.config :as common-config]
             [logseq.common.path :as path]
             [logseq.common.path :as path]
             [logseq.common.util :as common-util]
             [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.date-time :as date-time-util]
             [logseq.common.util.macro :as macro-util]
             [logseq.common.util.macro :as macro-util]
             [logseq.common.util.namespace :as ns-util]
             [logseq.common.util.namespace :as ns-util]
@@ -457,6 +458,8 @@
   "Special keywords in previous query table"
   "Special keywords in previous query table"
   {:page :block/title
   {:page :block/title
    :block :block/title
    :block :block/title
+   :tags :block/tags
+   :alias :block/alias
    :created-at :block/created-at
    :created-at :block/created-at
    :updated-at :block/updated-at})
    :updated-at :block/updated-at})
 
 
@@ -795,60 +798,140 @@
         (pr-str (dissoc query-map :title :group-by-page? :collapsed?))
         (pr-str (dissoc query-map :title :group-by-page? :collapsed?))
         query-str))))
         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
 (defn- handle-block-properties
   "Does everything page properties does and updates a couple of block specific attributes"
   "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}]
    {{: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)
   (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
     {: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*)))
        (and (seq property-classes) (seq (:block/refs block*)))
        ;; remove unused, nonexistent property page
        ;; remove unused, nonexistent property page
        (update :block/refs (fn [refs] (remove #(property-classes (keyword (:block/name %))) refs))))
        (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
 (defn- update-block-refs
   "Updates the attributes of a block ref as this is where a new page is defined. Also
   "Updates the attributes of a block ref as this is where a new page is defined. Also
@@ -906,21 +989,6 @@
   [path]
   [path]
   (re-find #"assets/.*$" 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]
 (defn- update-asset-links-in-block-title [block-title asset-name-to-uuids ignored-assets]
   (reduce (fn [acc [asset-name asset-uuid]]
   (reduce (fn [acc [asset-name asset-uuid]]
             (let [new-title (string/replace acc
             (let [new-title (string/replace acc
@@ -938,93 +1006,51 @@
           asset-name-to-uuids))
           asset-name-to-uuids))
 
 
 (defn- handle-assets-in-block
 (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))
                       ;;  (prn :asset-added! (node-path/basename asset-name) #_(get @assets asset-name))
                       ;;  (cljs.pprint/pprint asset-link)
                       ;;  (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"
   "If a block contains a quote, convert block to #Quote node"
   [block opts]
   [block opts]
   (if-let [ast-block (first (filter #(= "Quote" (first %)) (:block.temp/ast-blocks block)))]
   (if-let [ast-block (first (filter #(= "Quote" (first %)) (:block.temp/ast-blocks block)))]
@@ -1034,15 +1060,41 @@
             :block/tags [:logseq.class/Quote-block]})
             :block/tags [:logseq.class/Quote-block]})
     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
 (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}]
   [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*)
   ;; (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]}
         {: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]}
         {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]
         ;; :block/page should be [:block/page NAME]
         journal-page-created-at (some-> (:block/page block*) second journal-created-ats)
         journal-page-created-at (some-> (:block/page block*) second journal-created-ats)
         prepared-block (cond-> block-after-assets
         prepared-block (cond-> block-after-assets
@@ -1053,7 +1105,8 @@
                    (fix-block-name-lookup-ref page-names-to-uuids)
                    (fix-block-name-lookup-ref page-names-to-uuids)
                    (update-block-refs page-names-to-uuids options)
                    (update-block-refs page-names-to-uuids options)
                    (update-block-tags db (:user-options options) per-file-state (:all-idents import-state))
                    (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-marker options)
                    (update-block-priority options)
                    (update-block-priority options)
                    add-missing-timestamps
                    add-missing-timestamps
@@ -1344,17 +1397,26 @@
   "Separates new pages from new properties tx in preparation for properties to
   "Separates new pages from new properties tx in preparation for properties to
   be transacted separately. Also builds property pages tx and converts existing
   be transacted separately. Also builds property pages tx and converts existing
   pages that are now properties"
   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))
   (let [new-properties (set/difference (set (keys @(:property-schemas import-state))) (set old-properties))
         ;; _ (when (seq new-properties) (prn :new-properties new-properties))
         ;; _ (when (seq new-properties) (prn :new-properties new-properties))
         [properties-tx pages-tx'] ((juxt filter remove)
         [properties-tx pages-tx'] ((juxt filter remove)
                                    #(contains? new-properties (keyword (:block/name %))) pages-tx)
                                    #(contains? new-properties (keyword (:block/name %))) pages-tx)
         property-pages-tx (map (fn [{block-uuid :block/uuid :block/keys [title]}]
         property-pages-tx (map (fn [{block-uuid :block/uuid :block/keys [title]}]
                                  (let [property-name (keyword (string/lower-case 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)
                                properties-tx)
         converted-property-pages-tx
         converted-property-pages-tx
         (map (fn [kw-name]
         (map (fn [kw-name]
@@ -1508,7 +1570,8 @@
 
 
 (defn- save-from-tx
 (defn- save-from-tx
   "Save importer state from given txs"
   "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))]
   (when-let [nodes (seq (filter :block/name txs))]
     (swap! (:all-existing-page-uuids import-state) merge (into {} (map (juxt :block/uuid identity) nodes)))))
     (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 %))
                       :or {notify-user #(println "[WARNING]" (:msg %))
                            log-fn prn}
                            log-fn prn}
                       :as *options}]
                       :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)
         {:keys [pages blocks]} (extract-pages-and-blocks @conn file content options)
         tx-options (merge (build-tx-options options)
         tx-options (merge (build-tx-options options)
                           {:journal-created-ats (build-journal-created-ats pages)})
                           {:journal-created-ats (build-journal-created-ats pages)})
@@ -1547,7 +1610,7 @@
                                                 (assoc tx-options :whiteboard? (some? (seq whiteboard-pages)))))
                                                 (assoc tx-options :whiteboard? (some? (seq whiteboard-pages)))))
                        vec)
                        vec)
         {:keys [property-pages-tx property-page-properties-tx] pages-tx' :pages-tx}
         {: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}))
         ;; _ (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
         ;; 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})
         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
       ;; Counts
       ;; Includes journals as property values e.g. :logseq.property/deadline
       ;; 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 (= 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))))
       (is (= 4 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Task]] @conn))))
@@ -253,7 +253,7 @@
               set))))
               set))))
 
 
     (testing "user properties"
     (testing "user properties"
-      (is (= 19
+      (is (= 20
              (->> @conn
              (->> @conn
                   (d/q '[:find [(pull ?b [:db/ident]) ...]
                   (d/q '[:find [(pull ?b [:db/ident]) ...]
                          :where [?b :block/tags :logseq.class/Property]])
                          :where [?b :block/tags :logseq.class/Property]])
@@ -396,10 +396,10 @@
               :logseq.property/query "{:query (task todo doing)}"
               :logseq.property/query "{:query (task todo doing)}"
               :block/tags [:logseq.class/Query]
               :block/tags [:logseq.class/Query]
               :logseq.property.table/ordered-columns [:block/title]}
               :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")
           "Advanced query has correct query properties")
       (is (= "tasks with todo and doing"
       (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")
           "Advanced query has custom title migrated")
 
 
       ;; Cards
       ;; Cards
@@ -429,6 +429,26 @@
              (:block/title (db-test/find-block-by-content @conn #"Learn Datalog")))
              (:block/title (db-test/find-block-by-content @conn #"Learn Datalog")))
           "Imports full quote with various ast types"))
           "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"
     (testing "tags convert to classes"
       (is (= :user.class/Quotes___life
       (is (= :user.class/Quotes___life
              (:db/ident (db-test/find-page-by-title @conn "life")))
              (:db/ident (db-test/find-page-by-title @conn "life")))
@@ -503,7 +523,11 @@
         (is (= "20"
         (is (= "20"
                (:user.property/duration (db-test/readable-properties (db-test/find-block-by-content @conn "existing :number to :default"))))
                (: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")
             "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}
         (is (= {:logseq.property/type :default :db/cardinality :db.cardinality/many}
                (select-keys (d/entity @conn :user.property/people) [:logseq.property/type :db/cardinality]))
                (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")
             ":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
     :breadcrumb-show? true
     :collapsed? False
     :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
   #+BEGIN_QUOTE
   *Italic* ~~Strikethrough~~ ^^Highlight^^ #[[foo]]
   *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).
   **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")
                      :payload {:message (or message "Built-in pages can't be edited")
                                :type :warning}}))))
                                :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]
   [entity]
   (cond
   (cond
     (ldb/property? entity)
     (ldb/property? entity)
@@ -80,17 +62,6 @@
       [?b :block/tags ?tag-id]
       [?b :block/tags ?tag-id]
       [(missing? $ ?b :logseq.property/built-in?)]
       [(missing? $ ?b :logseq.property/built-in?)]
       [(not= ?b ?eid)]]
       [(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)
     (:block/parent entity)
     '[:find [?b ...]
     '[:find [?b ...]
       :in $ ?eid ?title [?tag-id ...]
       :in $ ?eid ?title [?tag-id ...]
@@ -112,10 +83,9 @@
 
 
 (defn- validate-unique-for-page
 (defn- validate-unique-for-page
   [db new-title {:block/keys [tags] :as entity}]
   [db new-title {:block/keys [tags] :as entity}]
-  (cond
-    (seq tags)
+  (when (seq tags)
     (when-let [another-id (first
     (when-let [another-id (first
-                           (d/q (another-id-q entity)
+                           (d/q (find-other-ids-with-title-and-tags entity)
                                 db
                                 db
                                 (:db/id entity)
                                 (:db/id entity)
                                 new-title
                                 new-title
@@ -127,20 +97,30 @@
         (when-not (and (= common-tag-ids #{:logseq.class/Page})
         (when-not (and (= common-tag-ids #{:logseq.class/Page})
                        (> (count this-tags) 1)
                        (> (count this-tags) 1)
                        (> (count another-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:
   "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 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]
   [db new-title entity]
   (when (entity-util/page? entity)
   (when (entity-util/page? entity)
     (validate-unique-for-page db new-title 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"
   "Validates a block title when it has changed for a entity-util/page? or tagged node"
   [db new-title existing-block-entity]
   [db new-title existing-block-entity]
   (validate-built-in-pages 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))
   (validate-disallow-page-with-journal-name new-title existing-block-entity))
 
 
 (defn validate-property-title
 (defn validate-property-title

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

@@ -11,7 +11,7 @@
                             :color2 {:logseq.property/type :default}}})]
                             :color2 {:logseq.property/type :default}}})]
 
 
     (is (nil?
     (is (nil?
-         (outliner-validate/validate-unique-by-name-tag-and-block-type
+         (outliner-validate/validate-unique-by-name-and-tags
           @conn
           @conn
           (:block/title (d/entity @conn :logseq.property/background-color))
           (:block/title (d/entity @conn :logseq.property/background-color))
           (d/entity @conn :user.property/color)))
           (d/entity @conn :user.property/color)))
@@ -19,13 +19,35 @@
 
 
     (is (thrown-with-msg?
     (is (thrown-with-msg?
          js/Error
          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
           @conn
           "color"
           "color"
           (d/entity @conn :user.property/color2)))
           (d/entity @conn :user.property/color2)))
         "Disallow duplicate user property")))
         "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
 (deftest validate-block-title-unique-for-pages
   (let [conn (db-test/create-conn-with-blocks
   (let [conn (db-test/create-conn-with-blocks
               [{:page {:block/title "page1"}}
               [{:page {:block/title "page1"}}
@@ -37,13 +59,13 @@
     (is (thrown-with-msg?
     (is (thrown-with-msg?
          js/Error
          js/Error
          #"Duplicate page"
          #"Duplicate page"
-         (outliner-validate/validate-unique-by-name-tag-and-block-type
+         (outliner-validate/validate-unique-by-name-and-tags
           @conn
           @conn
           "Apple"
           "Apple"
           (db-test/find-page-by-title @conn "Another Company")))
           (db-test/find-page-by-title @conn "Another Company")))
         "Disallow duplicate page with tag")
         "Disallow duplicate page with tag")
     (is (nil?
     (is (nil?
-         (outliner-validate/validate-unique-by-name-tag-and-block-type
+         (outliner-validate/validate-unique-by-name-and-tags
           @conn
           @conn
           "Apple"
           "Apple"
           (db-test/find-page-by-title @conn "Banana")))
           (db-test/find-page-by-title @conn "Banana")))
@@ -52,14 +74,14 @@
     (is (thrown-with-msg?
     (is (thrown-with-msg?
          js/Error
          js/Error
          #"Duplicate page"
          #"Duplicate page"
-         (outliner-validate/validate-unique-by-name-tag-and-block-type
+         (outliner-validate/validate-unique-by-name-and-tags
           @conn
           @conn
           "page1"
           "page1"
           (db-test/find-page-by-title @conn "another page")))
           (db-test/find-page-by-title @conn "another page")))
         "Disallow duplicate page without tag")
         "Disallow duplicate page without tag")
 
 
     (is (nil?
     (is (nil?
-         (outliner-validate/validate-unique-by-name-tag-and-block-type
+         (outliner-validate/validate-unique-by-name-and-tags
           @conn
           @conn
           "Apple"
           "Apple"
           (db-test/find-page-by-title @conn "Fruit")))
           (db-test/find-page-by-title @conn "Fruit")))
@@ -151,7 +173,7 @@
             page-errors (atom {})]
             page-errors (atom {})]
         (doseq [page pages]
         (doseq [page pages]
           (try
           (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 (:block/title page) {:node page})
             (outliner-validate/validate-page-title-characters (: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/pdfjs/pdf.mjs"></script>
 <script defer type="module" src="/static/js/pdf_viewer3.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/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/html2canvas.min.js"></script>
 <script defer src="/static/js/lsplugin.core.js"></script>
 <script defer src="/static/js/lsplugin.core.js"></script>
 <script defer src="/static/js/react.production.min.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/pdfjs/pdf.mjs"></script>
 <script defer type="module" src="./js/pdf_viewer3.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/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/html2canvas.min.js"></script>
 <script defer src="./js/lsplugin.core.js"></script>
 <script defer src="./js/lsplugin.core.js"></script>
 <script defer src="./js/react.production.min.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
           [:p.text-error.text-xs [:small.opacity-80
                                   (util/format "%s not found!" (string/capitalize type))]])))))
                                   (util/format "%s not found!" (string/capitalize type))]])))))
 
 
-(defn open-lightbox
+(defn open-lightbox!
   [e]
   [e]
   (let [images (js/document.querySelectorAll ".asset-container img")
   (let [images (js/document.querySelectorAll ".asset-container img")
         images (to-array images)
         images (to-array images)
@@ -307,83 +307,95 @@
 (defonce *resizing-image? (atom false))
 (defonce *resizing-image? (atom false))
 (rum/defc asset-container
 (rum/defc asset-container
   [asset-block src title metadata {:keys [breadcrumb? positioned? local? full-text]}]
   [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?)]
                   (let [*local-selected? (atom local?)]
                     (-> (shui/dialog-confirm!
                     (-> (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
 ;; TODO: store image height and width for better ux
 (rum/defcs ^:large-vars/cleanup-todo resizable-image <
 (rum/defcs ^:large-vars/cleanup-todo resizable-image <
@@ -396,32 +408,32 @@
         positioned? (:property-position config)
         positioned? (:property-position config)
         asset-block (:asset-block config)
         asset-block (:asset-block config)
         width (or (get-in asset-block [:logseq.property.asset/resize-metadata :width])
         width (or (get-in asset-block [:logseq.property.asset/resize-metadata :width])
-                  (:width metadata))
+                (:width metadata))
         *width (get state ::size)
         *width (get state ::size)
         width (or @*width width 250)
         width (or @*width width 250)
         metadata' (merge
         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?))
         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'
         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)
     (if (or (:disable-resize? config)
-            (not resizable?))
+          (not resizable?))
       asset-container-cp
       asset-container-cp
       [:div.ls-resize-image.rounded-md
       [:div.ls-resize-image.rounded-md
        asset-container-cp
        asset-container-cp
        (resize-image-handles
        (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
             (case k
               :start
               :start
@@ -689,10 +701,9 @@
 
 
    All page-names are sanitized except page-name-in-block"
    All page-names are sanitized except page-name-in-block"
   [state
   [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?]
            on-context-menu with-parent?]
-    :or {stop-click-event? true
-         with-parent? true}
+    :or {with-parent? true}
     :as config}
     :as config}
    page-entity children label]
    page-entity children label]
   (let [*hover? (::hover? state)
   (let [*hover? (::hover? state)
@@ -718,9 +729,6 @@
                         (editor-handler/block->data-transfer! page-name e true))
                         (editor-handler/block->data-transfer! page-name e true))
        :on-mouse-over #(reset! *hover? true)
        :on-mouse-over #(reset! *hover? true)
        :on-mouse-leave #(reset! *hover? false)
        :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]
        :on-pointer-down (fn [^js e]
                           (cond
                           (cond
                             (util/link? (.-target e))
                             (util/link? (.-target e))
@@ -741,7 +749,6 @@
                               (reset! *mouse-down? true))))
                               (reset! *mouse-down? true))))
        :on-pointer-up (fn [e]
        :on-pointer-up (fn [e]
                         (when @*mouse-down?
                         (when @*mouse-down?
-                          (util/stop e)
                           (state/clear-edit!)
                           (state/clear-edit!)
                           (when-not (:disable-click? config)
                           (when-not (:disable-click? config)
                             (<open-page-ref config page-entity e page-name contents-page?))
                             (<open-page-ref config page-entity e page-name contents-page?))
@@ -1550,9 +1557,10 @@
       (->elem
       (->elem
        :a
        :a
        (cond->
        (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
          title
          (assoc :title title))
          (assoc :title title))
        (map-inline config label)))
        (map-inline config label)))
@@ -1632,9 +1640,14 @@
                               href)]
                               href)]
                   (->elem
                   (->elem
                    :a
                    :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))
                      title (assoc :title title))
                    (map-inline config label))))))
                    (map-inline config label))))))
 
 
@@ -1903,7 +1916,7 @@
 
 
       (= name "namespace")
       (= name "namespace")
       (if (config/db-based-graph? (state/get-current-repo))
       (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)]
         (let [namespace (first arguments)]
           (when-not (string/blank? namespace)
           (when-not (string/blank? namespace)
             (let [namespace (string/lower-case (page-ref/get-page-name! namespace))
             (let [namespace (string/lower-case (page-ref/get-page-name! namespace))
@@ -2856,8 +2869,7 @@
                                                          (ui/icon "X" {:size 14})))
                                                          (ui/icon "X" {:size 14})))
                                                       (page-cp (assoc config
                                                       (page-cp (assoc config
                                                                       :tag? true
                                                                       :tag? true
-                                                                      :disable-preview? true
-                                                                      :stop-click-event? false) tag)]))
+                                                                      :disable-preview? true) tag)]))
                                                  popup-opts))}
                                                  popup-opts))}
            (for [tag (take 2 block-tags)]
            (for [tag (take 2 block-tags)]
              [:div.block-tag.pl-2
              [:div.block-tag.pl-2
@@ -3261,8 +3273,7 @@
                   rest)
                   rest)
         config (assoc config
         config (assoc config
                       :breadcrumb? true
                       :breadcrumb? true
-                      :disable-preview? true
-                      :stop-click-event? false)]
+                      :disable-preview? true)]
     (when (seq parents)
     (when (seq parents)
       (let [parents-props (doall
       (let [parents-props (doall
                            (for [{:block/keys [uuid name title] :as block} parents]
                            (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;
     @apply relative inline-block mt-2 w-full;
 
 
     .asset-action-bar {
     .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 {
     .asset-action-btn {
@@ -117,6 +120,12 @@
   }
   }
 }
 }
 
 
+.breadcrumb {
+  .property-value-inner[data-type], .property-value-inner .select-item {
+    display: inline;
+  }
+}
+
 .open-block-ref-link {
 .open-block-ref-link {
   background-color: var(--ls-page-properties-background-color);
   background-color: var(--ls-page-properties-background-color);
   padding: 1px 4px;
   padding: 1px 4px;
@@ -1118,7 +1127,7 @@ html.is-mac {
 }
 }
 
 
 .ls-resize-image {
 .ls-resize-image {
-  @apply flex relative;
+  @apply flex relative w-fit cursor-pointer;
 
 
   .handle-left, .handle-right {
   .handle-left, .handle-right {
     @apply absolute w-[6px] h-[15%] min-h-[30px] bg-black/30 hover:bg-black/70
     @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]
 (defn- matched-pages-with-new-page [partial-matched-pages db-tag? q]
   (if (or
   (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)))))
        (and db-tag? (some ldb/class? (:block/_alias (db/get-page q)))))
     partial-matched-pages
     partial-matched-pages
     (if db-tag?
     (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)}
       (cons {:block/title (str (t :new-page) " " q)}
             partial-matched-pages))))
             partial-matched-pages))))
 
 
@@ -757,6 +759,9 @@
       (and (= type :esc) (editor-handler/editor-commands-popup-exists?))
       (and (= type :esc) (editor-handler/editor-commands-popup-exists?))
       nil
       nil
 
 
+      (state/editor-in-composition?)
+      nil
+
       (or (contains?
       (or (contains?
            #{:commands :page-search :page-search-hashtag :block-search :template-search
            #{:commands :page-search :page-search-hashtag :block-search :template-search
              :property-search :property-value-search :datepicker}
              :property-search :property-value-search :datepicker}

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

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

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

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

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

@@ -1,14 +1,10 @@
 (ns frontend.extensions.lightbox
 (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!
 (defn preview-images!
   [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))]
        (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)))))))
          (rfe/push-state :page {:name (str (:block/uuid e))} (if id {:anchor (str "block-content-" + id)} nil)))))))
 
 
-(defn open-lightbox
+(defn open-lightbox!
   [e]
   [e]
   (let [images (js/document.querySelectorAll ".hl-area img")
   (let [images (js/document.querySelectorAll ".hl-area img")
         images (to-array images)
         images (to-array images)
@@ -443,7 +443,7 @@
               {:title (t :asset/maximize)
               {:title (t :asset/maximize)
                :tabIndex "-1"
                :tabIndex "-1"
                :on-pointer-down util/stop
                :on-pointer-down util/stop
-               :on-click open-lightbox}
+               :on-click open-lightbox!}
 
 
               (ui/icon "maximize")]]
               (ui/icon "maximize")]]
             [:img.w-full {:src @*src}]]])))))
             [: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"
    When returning false, this fn also displays appropriate notifications to the user"
   [repo block tag-entity]
   [repo block tag-entity]
   (try
   (try
-    (outliner-validate/validate-unique-by-name-tag-and-block-type
+    (outliner-validate/validate-unique-by-name-and-tags
      (db/get-db repo)
      (db/get-db repo)
      (:block/title block)
      (:block/title block)
      (update block :block/tags (fnil conj #{}) tag-entity))
      (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)
       (let [repo (state/get-current-repo)
             editor-state (assoc (get-state)
             editor-state (assoc (get-state)
                                 :block-id (:block/uuid next-block)
                                 :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)))))
         (delete-block-inner! repo editor-state)))))
 
 
 (defn keydown-delete-handler
 (defn keydown-delete-handler

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

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

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

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