diff --git a/clj-e2e/src/logseq/e2e/graph.clj b/clj-e2e/src/logseq/e2e/graph.clj
index f59a4feaf0..bf1ae6c801 100644
--- a/clj-e2e/src/logseq/e2e/graph.clj
+++ b/clj-e2e/src/logseq/e2e/graph.clj
@@ -2,9 +2,9 @@
(:require [clojure.edn :as edn]
[clojure.string :as string]
[logseq.e2e.assert :as assert]
+ [logseq.e2e.locator :as loc]
[logseq.e2e.util :as util]
- [wally.main :as w]
- [logseq.e2e.locator :as loc]))
+ [wally.main :as w]))
(defn- refresh-all-remote-graphs
[]
@@ -39,14 +39,11 @@
(defn remove-remote-graph
[graph-name]
(wait-for-remote-graph graph-name)
- (let [local-unlink-button-q
- (.first (w/-query (format "div[data-testid='logseq_db_%s'] a:has-text(\"Unlink (local)\")" graph-name)))]
- (if (.isVisible local-unlink-button-q)
- (do (w/click local-unlink-button-q)
- (w/click "div[role='alertdialog'] button:text('ok')")
- (remove-remote-graph graph-name))
- (do (w/click (format "div[data-testid='logseq_db_%s'] a:has-text(\"Remove (server)\")" graph-name))
- (w/click "div[role='alertdialog'] button:text('ok')")))))
+ (let [action-btn
+ (.first (w/-query (format "div[data-testid='logseq_db_%s'] .graph-action-btn" graph-name)))]
+ (w/click action-btn)
+ (w/click ".delete-remote-graph-menu-item")
+ (w/click "div[role='alertdialog'] button:text('ok')")))
(defn switch-graph
[to-graph-name wait-sync?]
diff --git a/clj-e2e/src/logseq/e2e/keyboard.clj b/clj-e2e/src/logseq/e2e/keyboard.clj
index 0ee0cfaf02..bebebda1d2 100644
--- a/clj-e2e/src/logseq/e2e/keyboard.clj
+++ b/clj-e2e/src/logseq/e2e/keyboard.clj
@@ -8,6 +8,7 @@
(def enter #(press "Enter"))
(def esc #(press "Escape"))
(def backspace #(press "Backspace"))
+(def delete #(press "Delete"))
(def tab #(press "Tab"))
(def shift+tab #(press "Shift+Tab"))
(def shift+enter #(press "Shift+Enter"))
diff --git a/clj-e2e/test/logseq/e2e/outliner_basic_test.clj b/clj-e2e/test/logseq/e2e/outliner_basic_test.clj
index db986bcf3f..7960fb1e37 100644
--- a/clj-e2e/test/logseq/e2e/outliner_basic_test.clj
+++ b/clj-e2e/test/logseq/e2e/outliner_basic_test.clj
@@ -65,6 +65,14 @@
(is (= "b1" (util/get-edit-content)))
(is (= 1 (util/page-blocks-count)))))
+(defn delete-end []
+ (testing "Delete at end"
+ (b/new-blocks ["b1" "b2" "b3"])
+ (k/arrow-up)
+ (k/delete)
+ (is (= "b2b3" (util/get-edit-content)))
+ (is (= 2 (util/page-blocks-count)))))
+
(defn delete-test-with-children []
(testing "Delete block with its children"
(b/new-blocks ["b1" "b2" "b3" "b4"])
@@ -88,5 +96,8 @@
(deftest delete-test
(delete))
+(deftest delete-end-test
+ (delete-end))
+
(deftest delete-test-with-children-test
(delete-test-with-children))
diff --git a/deps/db/src/logseq/db/frontend/malli_schema.cljs b/deps/db/src/logseq/db/frontend/malli_schema.cljs
index 085807cb24..d178742200 100644
--- a/deps/db/src/logseq/db/frontend/malli_schema.cljs
+++ b/deps/db/src/logseq/db/frontend/malli_schema.cljs
@@ -223,9 +223,15 @@
[:multi {:dispatch #(-> % first :logseq.property/type)}]
(map (fn [[prop-type value-schema]]
[prop-type
- (let [schema-fn (if (vector? value-schema) (last value-schema) value-schema)]
- [:fn (fn [tuple]
- (validate-property-value *db-for-validate-fns* schema-fn tuple))])])
+ (let [schema-fn (if (vector? value-schema) (last value-schema) value-schema)
+ error-message (when (vector? value-schema)
+ (and (map? (second value-schema))
+ (:error/message (second value-schema))))]
+ [:fn
+ (when error-message
+ {:error/message error-message})
+ (fn [tuple]
+ (validate-property-value *db-for-validate-fns* schema-fn tuple))])])
db-property-type/built-in-validation-schemas)))
(def block-properties
diff --git a/deps/db/src/logseq/db/frontend/property/type.cljs b/deps/db/src/logseq/db/frontend/property/type.cljs
index 98176e8066..f727268032 100644
--- a/deps/db/src/logseq/db/frontend/property/type.cljs
+++ b/deps/db/src/logseq/db/frontend/property/type.cljs
@@ -76,12 +76,12 @@
;; Validate && list fixes for non-validated values when updating property schema
(defn url?
- "Test if it is a `protocol://`-style URL.
+ "Test if it is a `protocol://`-style URL. Allows custom protocol such as `zotero`.
Originally from common-util/url? but does not need to be the same"
[s]
(and (string? s)
(try
- (not (contains? #{nil "null"} (.-origin (js/URL. s))))
+ (not (contains? #{nil} (.-origin (js/URL. s))))
(catch :default _e
false))))
diff --git a/deps/graph-parser/src/logseq/graph_parser/exporter.cljs b/deps/graph-parser/src/logseq/graph_parser/exporter.cljs
index 5e363f1d5b..aacc32135f 100644
--- a/deps/graph-parser/src/logseq/graph_parser/exporter.cljs
+++ b/deps/graph-parser/src/logseq/graph_parser/exporter.cljs
@@ -13,6 +13,7 @@
[logseq.common.config :as common-config]
[logseq.common.path :as path]
[logseq.common.util :as common-util]
+ [logseq.common.util.block-ref :as block-ref]
[logseq.common.util.date-time :as date-time-util]
[logseq.common.util.macro :as macro-util]
[logseq.common.util.namespace :as ns-util]
@@ -457,6 +458,8 @@
"Special keywords in previous query table"
{:page :block/title
:block :block/title
+ :tags :block/tags
+ :alias :block/alias
:created-at :block/created-at
:updated-at :block/updated-at})
@@ -795,60 +798,140 @@
(pr-str (dissoc query-map :title :group-by-page? :collapsed?))
query-str))))
+(defn- ast->text
+ "Given an ast block, convert it to text for use as a block title. This is a
+ slimmer version of handler.export.text/export-blocks-as-markdown"
+ [ast-block {:keys [log-fn]
+ :or {log-fn prn}}]
+ (let [extract
+ (fn extract [node]
+ (let [extract-emphasis
+ (fn extract-emphasis [node]
+ (let [[[type'] coll'] node]
+ (case type'
+ "Bold"
+ (vec (concat ["**"] (mapcat extract coll') ["**"]))
+ "Italic"
+ (vec (concat ["*"] (mapcat extract coll') ["*"]))
+ "Strike_through"
+ (vec (concat ["~~"] (mapcat extract coll') ["~~"]))
+ "Highlight"
+ (vec (concat ["^^"] (mapcat extract coll') ["^^"]))
+ (throw (ex-info (str "Failed to wrap Emphasis AST block of type " (pr-str type')) {})))))]
+ (cond
+ (and (vector? node) (#{"Inline_Html" "Plain" "Inline_Hiccup"} (first node)))
+ [(second node)]
+ (and (vector? node) (#{"Break_Line" "Hard_Break_Line"} (first node)))
+ ["\n"]
+ (and (vector? node) (= (first node) "Link"))
+ [(:full_text (second node))]
+ (and (vector? node) (#{"Paragraph" "Quote"} (first node)))
+ (mapcat extract (second node))
+ (and (vector? node) (= (first node) "Tag"))
+ (into ["#"] (mapcat extract (second node)))
+ (and (vector? node) (= (first node) "Emphasis"))
+ (extract-emphasis (second node))
+ (and (vector? node) (= ["Custom" "query"] (take 2 node)))
+ [(get node 4)]
+ (and (vector? node) (= (first node) "Code"))
+ ["`" (second node) "`"]
+ (and (vector? node) (= "Macro" (first node)) (= "query" (:name (second node))))
+ (:arguments (second node))
+ (and (vector? node) (= (first node) "Example"))
+ (second node)
+ :else
+ (do
+ (log-fn :ast->text "Ignored ast node" :node node)
+ []))))]
+ (->> (extract ast-block)
+ ;; ((fn [x] (prn :X x) x))
+ (apply str)
+ string/trim)))
+
+(defn- walk-ast-blocks
+ "Walks each ast block in order to its full depth. Saves multiple ast types for
+ use in build-block-tx. This walk is only done once for perf reasons"
+ [ast-blocks]
+ (let [results (atom {:simple-queries []
+ :asset-links []
+ :embeds []})]
+ (walk/prewalk
+ (fn [x]
+ (cond
+ (and (vector? x)
+ (= "Link" (first x))
+ (common-config/local-asset? (second (:url (second x)))))
+ (swap! results update :asset-links conj x)
+ (and (vector? x)
+ (= "Macro" (first x))
+ (= "embed" (:name (second x))))
+ (swap! results update :embeds conj x)
+ (and (vector? x)
+ (= "Macro" (first x))
+ (= "query" (:name (second x))))
+ (swap! results update :simple-queries conj x))
+ x)
+ ast-blocks)
+ @results))
+
+(defn- handle-queries
+ "If a block contains a simple or advanced queries, converts block to a #Query node"
+ [{:block/keys [title] :as block} db page-names-to-uuids walked-ast-blocks options]
+ (if-let [query (some-> (first (:simple-queries walked-ast-blocks))
+ (ast->text (select-keys options [:log-fn]))
+ string/trim)]
+ (let [props {:logseq.property/query query}
+ {:keys [block-properties pvalues-tx]}
+ (build-properties-and-values props db page-names-to-uuids
+ (select-keys block [:block/properties-text-values :block/name :block/title :block/uuid])
+ options)
+ block'
+ (-> (update block :block/tags (fnil conj []) :logseq.class/Query)
+ (merge block-properties
+ {:block/title (string/trim (string/replace-first title #"\{\{query(.*)\}\}" ""))}))]
+ {:block block'
+ :pvalues-tx pvalues-tx})
+ (if-let [advanced-query (some-> (first (filter #(= ["Custom" "query"] (take 2 %)) (:block.temp/ast-blocks block)))
+ (ast->text (select-keys options [:log-fn]))
+ string/trim)]
+ (let [props {:logseq.property/query (migrate-advanced-query-string advanced-query)}
+ {:keys [block-properties pvalues-tx]}
+ (build-properties-and-values props db page-names-to-uuids
+ (select-keys block [:block/properties-text-values :block/name :block/title :block/uuid])
+ options)
+ pvalues-tx'
+ (concat pvalues-tx [{:block/uuid (second (:logseq.property/query block-properties))
+ :logseq.property.code/lang "clojure"
+ :logseq.property.node/display-type :code}])
+ block'
+ (let [query-map (common-util/safe-read-map-string advanced-query)]
+ (cond-> (update block :block/tags (fnil conj []) :logseq.class/Query)
+ true
+ (merge block-properties)
+ true
+ (assoc :block/title
+ (or (when-let [title' (:title query-map)]
+ (if (string? title') title' (pr-str title')))
+ ;; Put all non-query content in title for now
+ (string/trim (string/replace-first title #"(?s)#\+BEGIN_QUERY(.*)#\+END_QUERY" ""))))
+ (:collapsed? query-map)
+ (assoc :block/collapsed? true)))]
+ {:block block'
+ :pvalues-tx pvalues-tx'})
+ {:block block})))
+
(defn- handle-block-properties
"Does everything page properties does and updates a couple of block specific attributes"
- [{:block/keys [title] :as block*}
- db page-names-to-uuids refs
+ [block* db page-names-to-uuids refs walked-ast-blocks
{{:keys [property-classes]} :user-options :as options}]
(let [{:keys [block properties-tx]} (handle-page-and-block-properties block* db page-names-to-uuids refs options)
- advanced-query (some->> (second (re-find #"(?s)#\+BEGIN_QUERY(.*)#\+END_QUERY" title)) string/trim)
- additional-props (cond-> {}
- ;; Order matters as we ensure a simple query gets priority
- (macro-util/query-macro? title)
- (assoc :logseq.property/query
- (or (some->> (second (re-find #"\{\{query(.*)\}\}" title))
- string/trim)
- title))
- (seq advanced-query)
- (assoc :logseq.property/query (migrate-advanced-query-string advanced-query)))
- {:keys [block-properties pvalues-tx]}
- (when (seq additional-props)
- (build-properties-and-values additional-props db page-names-to-uuids
- (select-keys block [:block/properties-text-values :block/name :block/title :block/uuid])
- options))
- pvalues-tx' (if (and pvalues-tx (seq advanced-query))
- (concat pvalues-tx [{:block/uuid (second (:logseq.property/query block-properties))
- :logseq.property.code/lang "clojure"
- :logseq.property.node/display-type :code}])
- pvalues-tx)]
+ {block' :block :keys [pvalues-tx]} (handle-queries block db page-names-to-uuids walked-ast-blocks options)]
{:block
- (cond-> block
- (seq block-properties)
- (merge block-properties)
-
- (macro-util/query-macro? title)
- ((fn [b]
- (merge (update b :block/tags (fnil conj []) :logseq.class/Query)
- ;; Put all non-query content in title. Could just be a blank string
- {:block/title (string/trim (string/replace-first title #"\{\{query(.*)\}\}" ""))})))
-
- (seq advanced-query)
- ((fn [b]
- (let [query-map (common-util/safe-read-map-string advanced-query)]
- (cond-> (update b :block/tags (fnil conj []) :logseq.class/Query)
- true
- (assoc :block/title
- (or (when-let [title' (:title query-map)]
- (if (string? title') title' (pr-str title')))
- ;; Put all non-query content in title for now
- (string/trim (string/replace-first title #"(?s)#\+BEGIN_QUERY(.*)#\+END_QUERY" ""))))
- (:collapsed? query-map)
- (assoc :block/collapsed? true)))))
-
+ (cond-> block'
(and (seq property-classes) (seq (:block/refs block*)))
;; remove unused, nonexistent property page
(update :block/refs (fn [refs] (remove #(property-classes (keyword (:block/name %))) refs))))
- :properties-tx (concat properties-tx (when pvalues-tx' pvalues-tx'))}))
+ :properties-tx (concat properties-tx (when pvalues-tx pvalues-tx))}))
(defn- update-block-refs
"Updates the attributes of a block ref as this is where a new page is defined. Also
@@ -906,21 +989,6 @@
[path]
(re-find #"assets/.*$" path))
-(defn- find-all-asset-links
- "Walks each ast block in order to its full depth as Link asts can be in different
- locations e.g. a Heading vs a Paragraph ast block"
- [ast-blocks]
- (let [results (atom [])]
- (walk/prewalk
- (fn [x]
- (when (and (vector? x)
- (= "Link" (first x))
- (common-config/local-asset? (second (:url (second x)))))
- (swap! results conj x))
- x)
- ast-blocks)
- @results))
-
(defn- update-asset-links-in-block-title [block-title asset-name-to-uuids ignored-assets]
(reduce (fn [acc [asset-name asset-uuid]]
(let [new-title (string/replace acc
@@ -938,93 +1006,51 @@
asset-name-to-uuids))
(defn- handle-assets-in-block
- [block {:keys [assets ignored-assets]}]
- (let [asset-links (find-all-asset-links (:block.temp/ast-blocks block))]
- (if (seq asset-links)
- (let [asset-maps
- (keep
- (fn [asset-link]
- (let [asset-name (-> asset-link second :url second asset-path->name)]
- (if-let [asset-data (and asset-name (get @assets asset-name))]
- (if (:block/uuid asset-data)
- {:asset-name-uuid [asset-name (:block/uuid asset-data)]}
- (let [new-block (sqlite-util/block-with-timestamps
- {:block/uuid (d/squuid)
- :block/order (db-order/gen-key)
- :block/page :logseq.class/Asset
- :block/parent :logseq.class/Asset})
- new-asset (merge new-block
- {:block/tags [:logseq.class/Asset]
- :logseq.property.asset/type (:type asset-data)
- :logseq.property.asset/checksum (:checksum asset-data)
- :logseq.property.asset/size (:size asset-data)
- :block/title (db-asset/asset-name->title (node-path/basename asset-name))}
- (when-let [metadata (not-empty (common-util/safe-read-map-string (:metadata (second asset-link))))]
- {:logseq.property.asset/resize-metadata metadata}))]
+ "If a block contains assets, creates them as #Asset nodes in the Asset page and references them in the block."
+ [block {:keys [asset-links]} {:keys [assets ignored-assets]}]
+ (if (seq asset-links)
+ (let [asset-maps
+ (keep
+ (fn [asset-link]
+ (let [asset-name (-> asset-link second :url second asset-path->name)]
+ (if-let [asset-data (and asset-name (get @assets asset-name))]
+ (if (:block/uuid asset-data)
+ {:asset-name-uuid [asset-name (:block/uuid asset-data)]}
+ (let [new-block (sqlite-util/block-with-timestamps
+ {:block/uuid (d/squuid)
+ :block/order (db-order/gen-key)
+ :block/page :logseq.class/Asset
+ :block/parent :logseq.class/Asset})
+ new-asset (merge new-block
+ {:block/tags [:logseq.class/Asset]
+ :logseq.property.asset/type (:type asset-data)
+ :logseq.property.asset/checksum (:checksum asset-data)
+ :logseq.property.asset/size (:size asset-data)
+ :block/title (db-asset/asset-name->title (node-path/basename asset-name))}
+ (when-let [metadata (not-empty (common-util/safe-read-map-string (:metadata (second asset-link))))]
+ {:logseq.property.asset/resize-metadata metadata}))]
;; (prn :asset-added! (node-path/basename asset-name) #_(get @assets asset-name))
;; (cljs.pprint/pprint asset-link)
- (swap! assets assoc-in [asset-name :block/uuid] (:block/uuid new-block))
- {:asset-name-uuid [asset-name (:block/uuid new-asset)]
- :asset new-asset}))
- (do
- (swap! ignored-assets conj
- {:reason "No asset data found for this asset path"
- :path (-> asset-link second :url second)
- :location {:block (:block/title block)}})
- nil))))
- asset-links)
- asset-blocks (keep :asset asset-maps)
- asset-names-to-uuids
- (into {} (map :asset-name-uuid asset-maps))]
- (cond-> {:block
- (update block :block/title update-asset-links-in-block-title asset-names-to-uuids ignored-assets)}
- (seq asset-blocks)
- (assoc :asset-blocks-tx asset-blocks)))
- {:block block})))
+ (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
+(defn- handle-quotes
"If a block contains a quote, convert block to #Quote node"
[block opts]
(if-let [ast-block (first (filter #(= "Quote" (first %)) (:block.temp/ast-blocks block)))]
@@ -1034,15 +1060,41 @@
:block/tags [:logseq.class/Quote-block]})
block))
+(defn- handle-embeds
+ "If a block contains page or block embeds, converts block to a :block/link based embed"
+ [block page-names-to-uuids {:keys [embeds]} {:keys [log-fn] :or {log-fn prn}}]
+ (if-let [embed-node (first embeds)]
+ (cond
+ (page-ref/page-ref? (str (first (:arguments (second embed-node)))))
+ (let [page-uuid (get-page-uuid page-names-to-uuids
+ (some-> (page-ref/get-page-name (first (:arguments (second embed-node))))
+ common-util/page-name-sanity-lc)
+ {:block block})]
+ (merge block
+ {:block/title ""
+ :block/link [:block/uuid page-uuid]}))
+ (block-ref/block-ref? (str (first (:arguments (second embed-node)))))
+ (let [block-uuid (uuid (block-ref/get-block-ref-id (first (:arguments (second embed-node)))))]
+ (merge block
+ {:block/title ""
+ :block/link [:block/uuid block-uuid]}))
+ :else
+ (do
+ (log-fn :invalid-embed-arguments "Ignore embed because of invalid arguments" :args (:arguments (second embed-node)))
+ block))
+ block))
+
(defn- build-block-tx
[db block* pre-blocks {:keys [page-names-to-uuids] :as per-file-state} {:keys [import-state journal-created-ats] :as options}]
;; (prn ::block-in block*)
- (let [;; needs to come before update-block-refs to detect new property schemas
+ (let [walked-ast-blocks (walk-ast-blocks (:block.temp/ast-blocks block*))
+ ;; needs to come before update-block-refs to detect new property schemas
{:keys [block properties-tx]}
- (handle-block-properties block* db page-names-to-uuids (:block/refs block*) options)
- {block-after-built-in-props :block deadline-properties-tx :properties-tx} (update-block-deadline-and-scheduled block page-names-to-uuids options)
+ (handle-block-properties block* db page-names-to-uuids (:block/refs block*) walked-ast-blocks options)
+ {block-after-built-in-props :block deadline-properties-tx :properties-tx}
+ (update-block-deadline-and-scheduled block page-names-to-uuids options)
{block-after-assets :block :keys [asset-blocks-tx]}
- (handle-assets-in-block block-after-built-in-props (select-keys import-state [:assets :ignored-assets]))
+ (handle-assets-in-block block-after-built-in-props walked-ast-blocks (select-keys import-state [:assets :ignored-assets]))
;; :block/page should be [:block/page NAME]
journal-page-created-at (some-> (:block/page block*) second journal-created-ats)
prepared-block (cond-> block-after-assets
@@ -1053,7 +1105,8 @@
(fix-block-name-lookup-ref page-names-to-uuids)
(update-block-refs page-names-to-uuids options)
(update-block-tags db (:user-options options) per-file-state (:all-idents import-state))
- (handle-quote-in-block (select-keys options [:log-fn]))
+ (handle-embeds page-names-to-uuids walked-ast-blocks (select-keys options [:log-fn]))
+ (handle-quotes (select-keys options [:log-fn]))
(update-block-marker options)
(update-block-priority options)
add-missing-timestamps
@@ -1344,17 +1397,26 @@
"Separates new pages from new properties tx in preparation for properties to
be transacted separately. Also builds property pages tx and converts existing
pages that are now properties"
- [pages-tx old-properties existing-pages import-state]
+ [pages-tx old-properties existing-pages import-state upstream-properties]
(let [new-properties (set/difference (set (keys @(:property-schemas import-state))) (set old-properties))
;; _ (when (seq new-properties) (prn :new-properties new-properties))
[properties-tx pages-tx'] ((juxt filter remove)
#(contains? new-properties (keyword (:block/name %))) pages-tx)
property-pages-tx (map (fn [{block-uuid :block/uuid :block/keys [title]}]
(let [property-name (keyword (string/lower-case title))
- db-ident (get-ident @(:all-idents import-state) property-name)]
- (sqlite-util/build-new-property db-ident
- (get-property-schema @(:property-schemas import-state) property-name)
- {:title title :block-uuid block-uuid})))
+ db-ident (get-ident @(:all-idents import-state) property-name)
+ upstream-property (get upstream-properties property-name)]
+ (sqlite-util/build-new-property
+ db-ident
+ ;; Tweak new properties that have upstream changes in flight to behave like
+ ;; existing properties i.e. they should be defined by the upstream property
+ (if (and upstream-property
+ (#{:date :node} (:from-type upstream-property))
+ (= :default (get-in upstream-property [:schema :logseq.property/type])))
+ ;; Assumes :many for :date and :node like infer-property-schema-and-get-property-change
+ {:logseq.property/type (:from-type upstream-property) :db/cardinality :many}
+ (get-property-schema @(:property-schemas import-state) property-name))
+ {:title title :block-uuid block-uuid})))
properties-tx)
converted-property-pages-tx
(map (fn [kw-name]
@@ -1508,7 +1570,8 @@
(defn- save-from-tx
"Save importer state from given txs"
- [txs {:keys [import-state]}]
+ [txs {:keys [import-state] :as _opts}]
+ ;; (when (string/includes? (:file _opts) "some-file.md") (cljs.pprint/pprint txs))
(when-let [nodes (seq (filter :block/name txs))]
(swap! (:all-existing-page-uuids import-state) merge (into {} (map (juxt :block/uuid identity) nodes)))))
@@ -1527,7 +1590,7 @@
:or {notify-user #(println "[WARNING]" (:msg %))
log-fn prn}
:as *options}]
- (let [options (assoc *options :notify-user notify-user :log-fn log-fn)
+ (let [options (assoc *options :notify-user notify-user :log-fn log-fn :file file)
{:keys [pages blocks]} (extract-pages-and-blocks @conn file content options)
tx-options (merge (build-tx-options options)
{:journal-created-ats (build-journal-created-ats pages)})
@@ -1547,7 +1610,7 @@
(assoc tx-options :whiteboard? (some? (seq whiteboard-pages)))))
vec)
{:keys [property-pages-tx property-page-properties-tx] pages-tx' :pages-tx}
- (split-pages-and-properties-tx pages-tx old-properties existing-pages (:import-state options))
+ (split-pages-and-properties-tx pages-tx old-properties existing-pages (:import-state options) @(:upstream-properties tx-options))
;; _ (when (seq property-pages-tx) (cljs.pprint/pprint {:property-pages-tx property-pages-tx}))
;; Necessary to transact new property entities first so that block+page properties can be transacted next
main-props-tx-report (d/transact! conn property-pages-tx {::new-graph? true ::path file})
diff --git a/deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs b/deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs
index b0cb8c6d5f..2967275b01 100644
--- a/deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs
+++ b/deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs
@@ -206,7 +206,7 @@
;; Counts
;; Includes journals as property values e.g. :logseq.property/deadline
- (is (= 26 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Journal]] @conn))))
+ (is (= 27 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Journal]] @conn))))
(is (= 3 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Asset]] @conn))))
(is (= 4 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Task]] @conn))))
@@ -253,7 +253,7 @@
set))))
(testing "user properties"
- (is (= 19
+ (is (= 20
(->> @conn
(d/q '[:find [(pull ?b [:db/ident]) ...]
:where [?b :block/tags :logseq.class/Property]])
@@ -396,10 +396,10 @@
:logseq.property/query "{:query (task todo doing)}"
:block/tags [:logseq.class/Query]
:logseq.property.table/ordered-columns [:block/title]}
- (db-test/readable-properties (db-test/find-block-by-content @conn #"tasks with")))
+ (db-test/readable-properties (db-test/find-block-by-content @conn #"tasks with todo")))
"Advanced query has correct query properties")
(is (= "tasks with todo and doing"
- (:block/title (db-test/find-block-by-content @conn #"tasks with")))
+ (:block/title (db-test/find-block-by-content @conn #"tasks with todo")))
"Advanced query has custom title migrated")
;; Cards
@@ -429,6 +429,26 @@
(:block/title (db-test/find-block-by-content @conn #"Learn Datalog")))
"Imports full quote with various ast types"))
+ (testing "embeds"
+ (is (= {:block/title ""}
+ (-> (d/q '[:find [(pull ?b [*]) ...]
+ :in $ ?title
+ :where [?b :block/link ?l] [?b :block/page ?bp] [?bp :block/journal-day 20250612] [?l :block/title ?title]]
+ @conn
+ "page embed")
+ first
+ (select-keys [:block/title])))
+ "Page embed linked correctly")
+ (is (= {:block/title ""}
+ (-> (d/q '[:find [(pull ?b [*]) ...]
+ :in $ ?title
+ :where [?b :block/link ?l] [?b :block/page ?bp] [?bp :block/journal-day 20250612] [?l :block/title ?title]]
+ @conn
+ "test block embed")
+ first
+ (select-keys [:block/title])))
+ "Block embed linked correctly"))
+
(testing "tags convert to classes"
(is (= :user.class/Quotes___life
(:db/ident (db-test/find-page-by-title @conn "life")))
@@ -503,7 +523,11 @@
(is (= "20"
(:user.property/duration (db-test/readable-properties (db-test/find-block-by-content @conn "existing :number to :default"))))
"existing :number property value correctly saved as :default")
+ (is (= :default
+ (:logseq.property/type (d/entity @conn :user.property/people2)))
+ ":node property changes to :default when :node is defined in same file")
+ ;; tests :node :many to :default transition after :node is defined in separate file
(is (= {:logseq.property/type :default :db/cardinality :db.cardinality/many}
(select-keys (d/entity @conn :user.property/people) [:logseq.property/type :db/cardinality]))
":node property to :default value changes to :default and keeps existing cardinality")
diff --git a/deps/graph-parser/test/resources/exporter-test-graph/journals/2024_08_07.md b/deps/graph-parser/test/resources/exporter-test-graph/journals/2024_08_07.md
index 884b030193..eae6224ac7 100644
--- a/deps/graph-parser/test/resources/exporter-test-graph/journals/2024_08_07.md
+++ b/deps/graph-parser/test/resources/exporter-test-graph/journals/2024_08_07.md
@@ -41,4 +41,19 @@
:breadcrumb-show? true
:collapsed? False
}
- #+END_QUERY
\ No newline at end of file
+ #+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
\ No newline at end of file
diff --git a/deps/graph-parser/test/resources/exporter-test-graph/journals/2025_06_12.md b/deps/graph-parser/test/resources/exporter-test-graph/journals/2025_06_12.md
index b54cde56f4..45a2a47898 100644
--- a/deps/graph-parser/test/resources/exporter-test-graph/journals/2025_06_12.md
+++ b/deps/graph-parser/test/resources/exporter-test-graph/journals/2025_06_12.md
@@ -16,4 +16,9 @@
#+BEGIN_QUOTE
*Italic* ~~Strikethrough~~ ^^Highlight^^ #[[foo]]
**Learn Datalog Today** is an interactive tutorial designed to teach you the [Datomic](http://datomic.com/) dialect of [Datalog](http://en.wikipedia.org/wiki/Datalog). Datalog is a declarative **database query language** with roots in logic programming. Datalog has similar expressive power as [SQL](http://en.wikipedia.org/wiki/Sql).
- #+END_QUOTE
\ No newline at end of file
+ #+END_QUOTE
+- test page embed
+ {{embed [[page embed]]}}
+- test block embed
+ id:: 685434e1-0bb9-468c-a660-1642b00b2854
+- {{embed ((685434e1-0bb9-468c-a660-1642b00b2854))}}
\ No newline at end of file
diff --git a/deps/graph-parser/test/resources/exporter-test-graph/journals/2025_06_23.md b/deps/graph-parser/test/resources/exporter-test-graph/journals/2025_06_23.md
new file mode 100644
index 0000000000..2f575485a0
--- /dev/null
+++ b/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
\ No newline at end of file
diff --git a/deps/outliner/src/logseq/outliner/validate.cljs b/deps/outliner/src/logseq/outliner/validate.cljs
index 1d1bde9f78..03050641f0 100644
--- a/deps/outliner/src/logseq/outliner/validate.cljs
+++ b/deps/outliner/src/logseq/outliner/validate.cljs
@@ -48,26 +48,8 @@
:payload {:message (or message "Built-in pages can't be edited")
:type :warning}}))))
-(defn- validate-unique-by-extends-and-name [db entity new-title]
- (when-let [_res (seq (d/q '[:find [?b ...]
- :in $ ?eid ?type ?title
- :where
- [?b :block/title ?title]
- [?b :logseq.property.class/extends ?type]
- [(not= ?b ?eid)]]
- db
- (:db/id entity)
- (:db/id (:logseq.property.class/extends entity))
- new-title))]
- (throw (ex-info "Duplicate page by parent"
- {:type :notification
- :payload {:message (str "Another page named " (pr-str new-title) " already exists for parents "
- (pr-str (->> (ldb/get-class-extends entity)
- (map :block/title)
- (string/join ns-util/parent-char))))
- :type :warning}}))))
-
-(defn- another-id-q
+(defn- find-other-ids-with-title-and-tags
+ "Query that finds other ids given the id to ignore, title to look up and tags to consider"
[entity]
(cond
(ldb/property? entity)
@@ -80,17 +62,6 @@
[?b :block/tags ?tag-id]
[(missing? $ ?b :logseq.property/built-in?)]
[(not= ?b ?eid)]]
- (:logseq.property.class/extends entity)
- '[:find [?b ...]
- :in $ ?eid ?title [?tag-id ...]
- :where
- [?b :block/title ?title]
- [?b :block/tags ?tag-id]
- [(not= ?b ?eid)]
- ;; same extends
- [?b :logseq.property.class/extends ?bp]
- [?eid :logseq.property.class/extends ?ep]
- [(= ?bp ?ep)]]
(:block/parent entity)
'[:find [?b ...]
:in $ ?eid ?title [?tag-id ...]
@@ -112,10 +83,9 @@
(defn- validate-unique-for-page
[db new-title {:block/keys [tags] :as entity}]
- (cond
- (seq tags)
+ (when (seq tags)
(when-let [another-id (first
- (d/q (another-id-q entity)
+ (d/q (find-other-ids-with-title-and-tags entity)
db
(:db/id entity)
new-title
@@ -127,20 +97,30 @@
(when-not (and (= common-tag-ids #{:logseq.class/Page})
(> (count this-tags) 1)
(> (count another-tags) 1))
- (throw (ex-info "Duplicate page"
- {:type :notification
- :payload {:message (str "Another page named " (pr-str new-title) " already exists for tags: "
- (string/join ", "
- (map (fn [id] (str "#" (:block/title (d/entity db id)))) common-tag-ids)))
- :type :warning}})))))
+ (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}}))))))))
- (: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
+(defn ^:api validate-unique-by-name-and-tags
"Validates uniqueness of nodes for the following cases:
- Page names are unique for a tag e.g. their can be Apple #Company and Apple #Fruit
- - Page names are unique for a :logseq.property.class/extends"
+ - Property names are unique with user properties being allowed to have the same name as built-in ones
+ - Class names are unique regardless of their extends or if they're built-in"
[db new-title entity]
(when (entity-util/page? entity)
(validate-unique-for-page db new-title entity)))
@@ -159,7 +139,7 @@
"Validates a block title when it has changed for a entity-util/page? or tagged node"
[db new-title existing-block-entity]
(validate-built-in-pages existing-block-entity)
- (validate-unique-by-name-tag-and-block-type db new-title existing-block-entity)
+ (validate-unique-by-name-and-tags db new-title existing-block-entity)
(validate-disallow-page-with-journal-name new-title existing-block-entity))
(defn validate-property-title
diff --git a/deps/outliner/test/logseq/outliner/validate_test.cljs b/deps/outliner/test/logseq/outliner/validate_test.cljs
index 3462f87b56..2561b3a61a 100644
--- a/deps/outliner/test/logseq/outliner/validate_test.cljs
+++ b/deps/outliner/test/logseq/outliner/validate_test.cljs
@@ -11,7 +11,7 @@
:color2 {:logseq.property/type :default}}})]
(is (nil?
- (outliner-validate/validate-unique-by-name-tag-and-block-type
+ (outliner-validate/validate-unique-by-name-and-tags
@conn
(:block/title (d/entity @conn :logseq.property/background-color))
(d/entity @conn :user.property/color)))
@@ -19,13 +19,35 @@
(is (thrown-with-msg?
js/Error
- #"Duplicate page"
- (outliner-validate/validate-unique-by-name-tag-and-block-type
+ #"Duplicate property"
+ (outliner-validate/validate-unique-by-name-and-tags
@conn
"color"
(d/entity @conn :user.property/color2)))
"Disallow duplicate user property")))
+(deftest validate-block-title-unique-for-tags
+ (let [conn (db-test/create-conn-with-blocks
+ {:classes {:Class1 {}
+ :Class2 {:logseq.property.class/extends :logseq.class/Task}}})]
+
+ (is (thrown-with-msg?
+ js/Error
+ #"Duplicate class"
+ (outliner-validate/validate-unique-by-name-and-tags
+ @conn
+ "Class1"
+ (d/entity @conn :user.class/Class2)))
+ "Disallow duplicate class names, regardless of extends")
+ (is (thrown-with-msg?
+ js/Error
+ #"Duplicate class"
+ (outliner-validate/validate-unique-by-name-and-tags
+ @conn
+ "Card"
+ (d/entity @conn :user.class/Class1)))
+ "Disallow duplicate class names even if it's built-in")))
+
(deftest validate-block-title-unique-for-pages
(let [conn (db-test/create-conn-with-blocks
[{:page {:block/title "page1"}}
@@ -37,13 +59,13 @@
(is (thrown-with-msg?
js/Error
#"Duplicate page"
- (outliner-validate/validate-unique-by-name-tag-and-block-type
+ (outliner-validate/validate-unique-by-name-and-tags
@conn
"Apple"
(db-test/find-page-by-title @conn "Another Company")))
"Disallow duplicate page with tag")
(is (nil?
- (outliner-validate/validate-unique-by-name-tag-and-block-type
+ (outliner-validate/validate-unique-by-name-and-tags
@conn
"Apple"
(db-test/find-page-by-title @conn "Banana")))
@@ -52,14 +74,14 @@
(is (thrown-with-msg?
js/Error
#"Duplicate page"
- (outliner-validate/validate-unique-by-name-tag-and-block-type
+ (outliner-validate/validate-unique-by-name-and-tags
@conn
"page1"
(db-test/find-page-by-title @conn "another page")))
"Disallow duplicate page without tag")
(is (nil?
- (outliner-validate/validate-unique-by-name-tag-and-block-type
+ (outliner-validate/validate-unique-by-name-and-tags
@conn
"Apple"
(db-test/find-page-by-title @conn "Fruit")))
@@ -151,7 +173,7 @@
page-errors (atom {})]
(doseq [page pages]
(try
- (outliner-validate/validate-unique-by-name-tag-and-block-type @conn (:block/title page) page)
+ (outliner-validate/validate-unique-by-name-and-tags @conn (:block/title page) page)
(outliner-validate/validate-page-title (:block/title page) {:node page})
(outliner-validate/validate-page-title-characters (:block/title page) {:node page})
diff --git a/public/index.html b/public/index.html
index 44f6b9064a..35f4d86ffe 100644
--- a/public/index.html
+++ b/public/index.html
@@ -53,6 +53,8 @@
+
+
diff --git a/resources/index.html b/resources/index.html
index e187af7252..3ca6e1f172 100644
--- a/resources/index.html
+++ b/resources/index.html
@@ -52,6 +52,8 @@ const portal = new MagicPortal(worker);
+
+
diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs
index b643c2c3b9..f09b45cd48 100644
--- a/src/main/frontend/components/block.cljs
+++ b/src/main/frontend/components/block.cljs
@@ -256,7 +256,7 @@
[:p.text-error.text-xs [:small.opacity-80
(util/format "%s not found!" (string/capitalize type))]])))))
-(defn open-lightbox
+(defn open-lightbox!
[e]
(let [images (js/document.querySelectorAll ".asset-container img")
images (to-array images)
@@ -307,83 +307,95 @@
(defonce *resizing-image? (atom false))
(rum/defc asset-container
[asset-block src title metadata {:keys [breadcrumb? positioned? local? full-text]}]
- [:div.asset-container
- {:key "resize-asset-container"}
- [:img.rounded-sm.relative
- (merge
- {:loading "lazy"
- :referrerPolicy "no-referrer"
- :src src
- :title title}
- metadata)]
- (when (and (not breadcrumb?)
- (not positioned?))
- [:<>
- (let [image-src (fs/asset-path-normalize src)]
- [:.asset-action-bar {:aria-hidden "true"}
- [:.flex
- (when-not config/publishing?
- [:button.asset-action-btn
- {:title (t :asset/delete)
- :tabIndex "-1"
- :on-pointer-down util/stop
- :on-click
- (fn [e]
- (util/stop e)
- (when-let [block-id (some-> (.-target e) (.closest "[blockid]") (.getAttribute "blockid") (uuid))]
+ (let [*el-ref (rum/use-ref nil)
+ image-src (fs/asset-path-normalize src)
+ get-blockid #(some-> (rum/deref *el-ref) (.closest "[blockid]") (.getAttribute "blockid") (uuid))]
+ [:div.asset-container
+ {:key "resize-asset-container"
+ :on-pointer-down util/stop
+ :on-click (fn [e]
+ (when (= "IMG" (some-> (.-target e) (.-nodeName)))
+ (open-lightbox! e)))
+ :ref *el-ref}
+ [:img.rounded-sm.relative
+ (merge
+ {:loading "lazy"
+ :referrerPolicy "no-referrer"
+ :src src
+ :title title}
+ metadata)]
+ (when (and (not breadcrumb?)
+ (not positioned?))
+ [:<>
+ (let [handle-copy!
+ (fn [_e]
+ (-> (util/copy-image-to-clipboard image-src)
+ (p/then #(notification/show! "Copied!" :success))))
+ handle-delete!
+ (fn [_e]
+ (when-let [block-id (get-blockid)]
(let [*local-selected? (atom local?)]
(-> (shui/dialog-confirm!
- [:div.text-xs.opacity-60.-my-2
- (when (and local? (not= (:block/uuid asset-block) block-id))
- [:label.flex.gap-1.items-center
- (shui/checkbox
- {:default-checked @*local-selected?
- :on-checked-change #(reset! *local-selected? %)})
- (t :asset/physical-delete)])]
- {:title (t :asset/confirm-delete (.toLocaleLowerCase (t :text/image)))
- :outside-cancel? true})
- (p/then (fn []
- (shui/dialog-close!)
- (editor-handler/delete-asset-of-block!
- {:block-id block-id
- :asset-block asset-block
- :local? local?
- :delete-local? @*local-selected?
- :repo (state/get-current-repo)
- :href src
- :title title
- :full-text full-text})))))))}
- (ui/icon "trash")])
+ [: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))]))
- [: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")])]])])])
+ (when-not config/publishing?
+ [:<>
+ (shui/dropdown-menu-separator)
+ (shui/dropdown-menu-item
+ {:on-click handle-delete!}
+ [:span.flex.items-center.gap-1.text-red-700
+ (ui/icon "trash") (t :asset/delete)])])
+ ])
+ {:align :start}))}
+ (shui/tabler-icon "dots-vertical")))])])]))
;; TODO: store image height and width for better ux
(rum/defcs ^:large-vars/cleanup-todo resizable-image <
@@ -396,32 +408,32 @@
positioned? (:property-position config)
asset-block (:asset-block config)
width (or (get-in asset-block [:logseq.property.asset/resize-metadata :width])
- (:width metadata))
+ (:width metadata))
*width (get state ::size)
width (or @*width width 250)
metadata' (merge
- (cond->
- {:height 125}
- width
- (assoc :width width))
- metadata)
+ (cond->
+ {:height 125}
+ width
+ (assoc :width width))
+ metadata)
resizable? (and (not (mobile-util/native-platform?))
- (not breadcrumb?)
- (not positioned?))
+ (not breadcrumb?)
+ (not positioned?))
asset-container-cp (asset-container asset-block src title metadata'
- {:breadcrumb? breadcrumb?
- :positioned? positioned?
- :local? local?
- :full-text full-text})]
+ {:breadcrumb? breadcrumb?
+ :positioned? positioned?
+ :local? local?
+ :full-text full-text})]
(if (or (:disable-resize? config)
- (not resizable?))
+ (not resizable?))
asset-container-cp
[:div.ls-resize-image.rounded-md
asset-container-cp
(resize-image-handles
- (fn [k ^js event]
- (let [dx (.-dx event)
- ^js target (.-target event)]
+ (fn [k ^js event]
+ (let [dx (.-dx event)
+ ^js target (.-target event)]
(case k
:start
@@ -689,10 +701,9 @@
All page-names are sanitized except page-name-in-block"
[state
- {:keys [contents-page? whiteboard-page? other-position? show-unique-title? stop-click-event?
+ {:keys [contents-page? whiteboard-page? other-position? show-unique-title?
on-context-menu with-parent?]
- :or {stop-click-event? true
- with-parent? true}
+ :or {with-parent? true}
:as config}
page-entity children label]
(let [*hover? (::hover? state)
@@ -718,9 +729,6 @@
(editor-handler/block->data-transfer! page-name e true))
:on-mouse-over #(reset! *hover? true)
:on-mouse-leave #(reset! *hover? false)
- :on-click (fn [e]
- (when (and stop-click-event? (not (util/link? (.-target e))))
- (util/stop e)))
:on-pointer-down (fn [^js e]
(cond
(util/link? (.-target e))
@@ -741,7 +749,6 @@
(reset! *mouse-down? true))))
:on-pointer-up (fn [e]
(when @*mouse-down?
- (util/stop e)
(state/clear-edit!)
(when-not (:disable-click? config)
(elem
:a
(cond->
- {:href (path/path-join "file://" path)
- :data-href path
- :target "_blank"}
+ {:on-click (fn [e]
+ (util/stop e)
+ (js/window.apis.openPath path))
+ :data-href path}
title
(assoc :title title))
(map-inline config label)))
@@ -1632,9 +1640,14 @@
href)]
(->elem
:a
- (cond-> {:href (path/path-join "file://" href*)
- :data-href href*
- :target "_blank"}
+ (cond-> (if (util/electron?)
+ {:on-click (fn [e]
+ (util/stop e)
+ (js/window.apis.openPath path))
+ :data-href href*}
+ {:href (path/path-join "file://" href*)
+ :data-href href*
+ :target "_blank"})
title (assoc :title title))
(map-inline config label))))))
@@ -1903,7 +1916,7 @@
(= name "namespace")
(if (config/db-based-graph? (state/get-current-repo))
- [:div.warning "Namespace is deprecated, use tags instead"]
+ [:div.warning (str "{{namespace}} is deprecated. Use the " common-config/library-page-name " feature instead.")]
(let [namespace (first arguments)]
(when-not (string/blank? namespace)
(let [namespace (string/lower-case (page-ref/get-page-name! namespace))
@@ -2856,8 +2869,7 @@
(ui/icon "X" {:size 14})))
(page-cp (assoc config
:tag? true
- :disable-preview? true
- :stop-click-event? false) tag)]))
+ :disable-preview? true) tag)]))
popup-opts))}
(for [tag (take 2 block-tags)]
[:div.block-tag.pl-2
@@ -3261,8 +3273,7 @@
rest)
config (assoc config
:breadcrumb? true
- :disable-preview? true
- :stop-click-event? false)]
+ :disable-preview? true)]
(when (seq parents)
(let [parents-props (doall
(for [{:block/keys [uuid name title] :as block} parents]
diff --git a/src/main/frontend/components/block.css b/src/main/frontend/components/block.css
index c82cbe9871..9a4dee9965 100644
--- a/src/main/frontend/components/block.css
+++ b/src/main/frontend/components/block.css
@@ -29,8 +29,11 @@
@apply relative inline-block mt-2 w-full;
.asset-action-bar {
- @apply top-0.5 right-0.5 absolute flex items-center
- border bg-gray-02 rounded opacity-0 transition-opacity;
+ @apply top-1 right-1 absolute flex items-center opacity-0 transition-opacity;
+
+ &[data-popup-active] {
+ @apply opacity-100;
+ }
}
.asset-action-btn {
@@ -117,6 +120,12 @@
}
}
+.breadcrumb {
+ .property-value-inner[data-type], .property-value-inner .select-item {
+ display: inline;
+ }
+}
+
.open-block-ref-link {
background-color: var(--ls-page-properties-background-color);
padding: 1px 4px;
@@ -1118,7 +1127,7 @@ html.is-mac {
}
.ls-resize-image {
- @apply flex relative;
+ @apply flex relative w-fit cursor-pointer;
.handle-left, .handle-right {
@apply absolute w-[6px] h-[15%] min-h-[30px] bg-black/30 hover:bg-black/70
diff --git a/src/main/frontend/components/editor.cljs b/src/main/frontend/components/editor.cljs
index 58796bf55a..933c74689f 100644
--- a/src/main/frontend/components/editor.cljs
+++ b/src/main/frontend/components/editor.cljs
@@ -136,18 +136,20 @@
(defn- matched-pages-with-new-page [partial-matched-pages db-tag? q]
(if (or
- (if db-tag?
- (let [entity (db/get-page q)]
- (and (ldb/internal-page? entity) (= (:block/title entity) q)))
- ;; Page existence here should be the same as entity-util/page?.
- ;; Don't show 'New page' if a page has any of these tags
- (db/page-exists? q db-class/page-classes))
-
+ (db/page-exists? q (if db-tag?
+ #{:logseq.class/Tag}
+ ;; Page existence here should be the same as entity-util/page?.
+ ;; Don't show 'New page' if a page has any of these tags
+ db-class/page-classes))
(and db-tag? (some ldb/class? (:block/_alias (db/get-page q)))))
partial-matched-pages
(if db-tag?
- (concat [{:block/title (str (t :new-tag) " " q)}]
- partial-matched-pages)
+ (concat
+ ;; Don't show 'New tag' for an internal page because it already shows 'Convert ...'
+ (when-not (let [entity (db/get-page q)]
+ (and (ldb/internal-page? entity) (= (:block/title entity) q)))
+ [{:block/title (str (t :new-tag) " " q)}])
+ partial-matched-pages)
(cons {:block/title (str (t :new-page) " " q)}
partial-matched-pages))))
@@ -757,6 +759,9 @@
(and (= type :esc) (editor-handler/editor-commands-popup-exists?))
nil
+ (state/editor-in-composition?)
+ nil
+
(or (contains?
#{:commands :page-search :page-search-hashtag :block-search :template-search
:property-search :property-value-search :datepicker}
diff --git a/src/main/frontend/components/property/value.cljs b/src/main/frontend/components/property/value.cljs
index 1dd32c22d8..fc183d86e4 100644
--- a/src/main/frontend/components/property/value.cljs
+++ b/src/main/frontend/components/property/value.cljs
@@ -393,7 +393,7 @@
(rum/defc overdue
[date content]
- (let [[current-time set-current-time!] (rum/use-state (t/now))]
+ (let [[current-time set-current-time!] (hooks/use-state (t/now))]
(hooks/use-effect!
(fn []
(let [timer (js/setInterval (fn [] (set-current-time! (t/now))) (* 1000 60 3))]
@@ -455,7 +455,7 @@
(rum/defc date-picker
[value {:keys [block property datetime? on-change on-delete del-btn? editing? multiple-values? other-position?]}]
- (let [*el (rum/use-ref nil)
+ (let [*el (hooks/use-ref nil)
content-fn (fn [{:keys [id]}] (calendar-inner id
{:block block
:property property
@@ -836,8 +836,8 @@
(rum/defc property-value-select-node < rum/static
[block property opts
{:keys [*show-new-property-config?]}]
- (let [[initial-choices set-initial-choices!] (rum/use-state nil)
- [result set-result!] (rum/use-state nil)
+ (let [[initial-choices set-initial-choices!] (hooks/use-state nil)
+ [result set-result!] (hooks/use-state nil)
set-result-and-initial-choices! (fn [value]
(set-initial-choices! value)
(set-result! value))
@@ -1144,7 +1144,7 @@
(rum/defc single-value-select
[block property value select-opts {:keys [value-render] :as opts}]
- (let [*el (rum/use-ref nil)
+ (let [*el (hooks/use-ref nil)
editing? (:editing? opts)
type (:logseq.property/type property)
select-opts' (assoc select-opts :multiple-choices? false)
@@ -1227,11 +1227,12 @@
(rum/defc single-number-input
[block property value-block table-view?]
- (let [[editing? set-editing!] (rum/use-state false)
- *ref (rum/use-ref nil)
- *input-ref (rum/use-ref nil)
+ (let [[editing? set-editing!] (hooks/use-state false)
+ *ref (hooks/use-ref nil)
+ *input-ref (hooks/use-ref nil)
number-value (db-property/property-value-content value-block)
- [value set-value!] (rum/use-state number-value)
+ [value set-value!] (hooks/use-state number-value)
+ [*value _] (hooks/use-state (atom value))
set-property-value! (fn [value & {:keys [exit-editing?]
:or {exit-editing? true}}]
(p/do!
@@ -1244,6 +1245,10 @@
(when exit-editing?
(set-editing! false))))]
+ (hooks/use-effect!
+ (fn []
+ #(set-property-value! @*value))
+ [])
[:div.ls-number.flex.flex-1.jtrigger
{:ref *ref
:on-click #(do
@@ -1256,8 +1261,11 @@
:class (str "ls-number-input h-6 px-0 py-0 border-none bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 text-base"
(when table-view? " text-sm"))
:value value
- :on-change (fn [e] (set-value! (util/evalue e)))
- :on-blur (fn [_e] (set-property-value! value))
+ :on-change (fn [e]
+ (set-value! (util/evalue e))
+ (reset! *value (util/evalue e)))
+ :on-blur (fn [_e]
+ (set-property-value! value))
:on-key-down (fn [e]
(let [input (rum/deref *input-ref)
pos (cursor/pos input)
@@ -1369,7 +1377,7 @@
[block property v {:keys [on-chosen editing?] :as opts}]
(let [type (:logseq.property/type property)
date? (= type :date)
- *el (rum/use-ref nil)
+ *el (hooks/use-ref nil)
items (cond->> (if (entity-map? v) #{v} v)
(= (:db/ident property) :block/tags)
(remove (fn [v] (contains? ldb/hidden-tags (:db/ident v)))))
diff --git a/src/main/frontend/components/repo.cljs b/src/main/frontend/components/repo.cljs
index 24b3a4055b..d27920e886 100644
--- a/src/main/frontend/components/repo.cljs
+++ b/src/main/frontend/components/repo.cljs
@@ -102,13 +102,14 @@
{:asChild true}
(shui/button
{:variant "ghost"
- :class "!px-1"
+ :class "graph-action-btn !px-1"
:size :sm}
(ui/icon "dots" {:size 15})))
(shui/dropdown-menu-content
{:align "end"}
(shui/dropdown-menu-item
{:key "delete-locally"
+ :class "delete-local-graph-menu-item"
:on-click (fn []
(let [prompt-str (if db-based?
(str "Are you sure to permanently delete the graph \"" graph-name "\" from Logseq?")
@@ -122,10 +123,11 @@
(p/then (fn []
(repo-handler/remove-repo! repo)
(state/pub-event! [:graph/unlinked repo (state/get-current-repo)]))))))}
- "Delete")
+ "Delete local graph")
(when (and remote? (or (and db-based? manager?) (not db-based?)))
(shui/dropdown-menu-item
{:key "delete-remotely"
+ :class "delete-remote-graph-menu-item"
:on-click (fn []
(let [prompt-str (str "Are you sure to permanently delete the graph \"" graph-name "\" from our server?")]
(-> (shui/dialog-confirm!
diff --git a/src/main/frontend/extensions/lightbox.cljs b/src/main/frontend/extensions/lightbox.cljs
index 18ef1a8d56..7898993b2f 100644
--- a/src/main/frontend/extensions/lightbox.cljs
+++ b/src/main/frontend/extensions/lightbox.cljs
@@ -1,14 +1,10 @@
(ns frontend.extensions.lightbox
- (:require [promesa.core :as p]
- [cljs-bean.core :as bean]
- [frontend.util :as util]))
+ (:require [cljs-bean.core :as bean]))
(defn preview-images!
[images]
- (p/let [_ (util/js-load$ (str util/JS_ROOT "/photoswipe.umd.min.js"))
- _ (util/js-load$ (str util/JS_ROOT "/photoswipe-lightbox.umd.min.js"))]
- (let [options {:dataSource images :pswpModule js/window.PhotoSwipe :showHideAnimationType "fade"}
- ^js lightbox (js/window.PhotoSwipeLightbox. (bean/->js options))]
- (doto lightbox
- (.init)
- (.loadAndOpen 0)))))
+ (let [options {:dataSource images :pswpModule js/window.PhotoSwipe :showHideAnimationType "fade"}
+ ^js lightbox (js/window.PhotoSwipeLightbox. (bean/->js options))]
+ (doto lightbox
+ (.init)
+ (.loadAndOpen 0))))
diff --git a/src/main/frontend/extensions/pdf/assets.cljs b/src/main/frontend/extensions/pdf/assets.cljs
index e5cefc6696..6e13b001e7 100644
--- a/src/main/frontend/extensions/pdf/assets.cljs
+++ b/src/main/frontend/extensions/pdf/assets.cljs
@@ -380,7 +380,7 @@
(when-let [e (some->> (:key current) (str "hls__") (db-model/get-page))]
(rfe/push-state :page {:name (str (:block/uuid e))} (if id {:anchor (str "block-content-" + id)} nil)))))))
-(defn open-lightbox
+(defn open-lightbox!
[e]
(let [images (js/document.querySelectorAll ".hl-area img")
images (to-array images)
@@ -443,7 +443,7 @@
{:title (t :asset/maximize)
:tabIndex "-1"
:on-pointer-down util/stop
- :on-click open-lightbox}
+ :on-click open-lightbox!}
(ui/icon "maximize")]]
[:img.w-full {:src @*src}]]])))))
diff --git a/src/main/frontend/handler/db_based/page.cljs b/src/main/frontend/handler/db_based/page.cljs
index 31cc75a13c..e8bfeb414e 100644
--- a/src/main/frontend/handler/db_based/page.cljs
+++ b/src/main/frontend/handler/db_based/page.cljs
@@ -23,7 +23,7 @@
When returning false, this fn also displays appropriate notifications to the user"
[repo block tag-entity]
(try
- (outliner-validate/validate-unique-by-name-tag-and-block-type
+ (outliner-validate/validate-unique-by-name-and-tags
(db/get-db repo)
(:block/title block)
(update block :block/tags (fnil conj #{}) tag-entity))
diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs
index 63074fe637..847978beaa 100644
--- a/src/main/frontend/handler/editor.cljs
+++ b/src/main/frontend/handler/editor.cljs
@@ -2706,7 +2706,10 @@
(let [repo (state/get-current-repo)
editor-state (assoc (get-state)
:block-id (:block/uuid next-block)
- :value (:block/title next-block))]
+ :value (:block/title next-block)
+ :block-container (util/get-next-block-non-collapsed
+ (util/rec-get-node (state/get-input) "ls-block")
+ {:exclude-property? true}))]
(delete-block-inner! repo editor-state)))))
(defn keydown-delete-handler
diff --git a/src/main/frontend/worker/db/migrate.cljs b/src/main/frontend/worker/db/migrate.cljs
index 08007905cc..083daf0afd 100644
--- a/src/main/frontend/worker/db/migrate.cljs
+++ b/src/main/frontend/worker/db/migrate.cljs
@@ -119,12 +119,12 @@
(defn rename-properties
[props-to-rename & {:keys [replace-fn]}]
- (fn [conn]
- (when (ldb/db-based-graph? @conn)
- (let [props-tx (rename-properties-aux @conn props-to-rename)
+ (fn [db]
+ (when (ldb/db-based-graph? db)
+ (let [props-tx (rename-properties-aux db props-to-rename)
fix-tx (mapcat (fn [[old new]]
;; can't use datoms b/c user properties aren't indexed
- (->> (d/q '[:find ?b ?prop-v :in $ ?prop :where [?b ?prop ?prop-v]] @conn old)
+ (->> (d/q '[:find ?b ?prop-v :in $ ?prop :where [?b ?prop ?prop-v]] db old)
(mapcat (fn [[id prop-value]]
(if (fn? replace-fn)
(replace-fn id prop-value)
@@ -136,10 +136,10 @@
(comment
(defn- rename-classes
[classes-to-rename]
- (fn [conn _search-db]
- (when (ldb/db-based-graph? @conn)
+ (fn [db]
+ (when (ldb/db-based-graph? db)
(mapv (fn [[old new]]
- (merge {:db/id (:db/id (d/entity @conn old))
+ (merge {:db/id (:db/id (d/entity db old))
:db/ident new}
(when-let [new-title (get-in db-class/built-in-classes [new :title])]
{:block/title new-title
@@ -147,9 +147,8 @@
classes-to-rename)))))
(defn fix-rename-parent-to-extends
- [conn _search-db]
- (let [db @conn
- parent-entity (d/entity db :logseq.property/parent)]
+ [db]
+ (let [parent-entity (d/entity db :logseq.property/parent)]
(when parent-entity
(let [old-p :logseq.property/parent
new-p :logseq.property.class/extends
@@ -160,7 +159,7 @@
new-p' (if (ldb/class? page) new-p :block/parent)]
[[:db/retract id old-p]
[:db/add id new-p' prop-value]]))})
- rename-property-tx (f conn)
+ rename-property-tx (f db)
library-page (if-let [page (ldb/get-built-in-page db common-config/library-page-name)]
page
(-> (sqlite-util/build-new-page common-config/library-page-name)
@@ -210,11 +209,10 @@
[:db/retract id :logseq.property/enable-history?]])
(defn separate-classes-and-properties
- [conn _sqlite-db]
+ [db]
;; find all properties that're classes, create new properties to separate them
;; from classes.
- (let [db @conn
- class-ids (d/q
+ (let [class-ids (d/q
'[:find [?b ...]
:where
[?b :block/tags :logseq.class/Property]
@@ -253,10 +251,9 @@
class-ids)))
(defn fix-tag-properties
- [conn _sqlite-db]
+ [db]
;; find all classes that're still used as properties
- (let [db @conn
- class-ids (d/q
+ (let [class-ids (d/q
'[:find [?b ...]
:where
[?b :block/tags :logseq.class/Tag]
@@ -275,9 +272,8 @@
class-ids)))
(defn add-missing-db-ident-for-tags
- [conn _sqlite-db]
- (let [db @conn
- class-ids (d/q
+ [db _sqlite-db]
+ (let [class-ids (d/q
'[:find [?b ...]
:where
[?b :block/tags :logseq.class/Tag]
@@ -294,10 +290,9 @@
class-ids)))
(defn fix-using-properties-as-tags
- [conn _sqlite-db]
+ [db]
;; find all properties that're tags
- (let [db @conn
- property-ids (->>
+ (let [property-ids (->>
(d/q
'[:find ?b ?i
:where
@@ -308,7 +303,7 @@
(map first))]
(mapcat
(fn [id]
- (let [property (d/entity @conn id)
+ (let [property (d/entity db id)
title (:block/title property)]
(into (retract-property-attributes id)
[[:db/retract id :logseq.property/parent]
@@ -316,10 +311,9 @@
property-ids)))
(defn remove-block-order-for-tags
- [conn _sqlite-db]
+ [db]
;; find all properties that're tags
- (let [db @conn
- tag-ids (d/q
+ (let [tag-ids (d/q
'[:find [?b ...]
:where
[?b :block/tags :logseq.class/Tag]
@@ -408,7 +402,7 @@
:db-migrate? true})))
(defn- upgrade-version!
- [conn search-db db-based? version {:keys [properties classes fix]}]
+ [conn db-based? version {:keys [properties classes fix]}]
(let [version (db-schema/parse-schema-version version)
db @conn
new-properties (->> (select-keys db-property/built-in-properties properties)
@@ -431,7 +425,7 @@
(when-let [db-ident (:db/ident class)]
{:db/ident db-ident})) new-classes)
fixes (when (fn? fix)
- (fix conn search-db))
+ (fix db))
tx-data (if db-based? (concat new-class-idents new-properties new-classes fixes) fixes)
tx-data' (concat
[(sqlite-util/kv :logseq.kv/schema-version version)]
@@ -442,7 +436,7 @@
(defn migrate
"Migrate 'frontend' datascript schema and data. To add a new migration,
add an entry to schema-version->updates and bump db-schema/version"
- [conn search-db]
+ [conn]
(when (ldb/db-based-graph? @conn)
(let [db @conn
version-in-db (db-schema/parse-schema-version (or (:kv/value (d/entity db :logseq.kv/schema-version)) 0))
@@ -465,7 +459,7 @@
schema-version->updates)]
(println "DB schema migrated from" version-in-db)
(doseq [[v m] updates]
- (upgrade-version! conn search-db db-based? v m))
+ (upgrade-version! conn db-based? v m))
(ensure-built-in-data-exists! conn))
(catch :default e
(prn :error (str "DB migration failed to migrate to " db-schema/version " from " version-in-db ":"))
diff --git a/src/main/frontend/worker/db_worker.cljs b/src/main/frontend/worker/db_worker.cljs
index 305687367d..29d3719e0d 100644
--- a/src/main/frontend/worker/db_worker.cljs
+++ b/src/main/frontend/worker/db_worker.cljs
@@ -296,7 +296,7 @@
(gc-sqlite-dbs! db client-ops-db conn {})
- (db-migrate/migrate conn search-db)
+ (db-migrate/migrate conn)
(db-listener/listen-db-changes! repo (get @*datascript-conns repo))))))
diff --git a/src/resources/dicts/en.edn b/src/resources/dicts/en.edn
index 2452d33d39..619c48a98a 100644
--- a/src/resources/dicts/en.edn
+++ b/src/resources/dicts/en.edn
@@ -176,6 +176,7 @@
:asset/copy "Copy image"
:asset/maximize "Maximize image"
:asset/ref-block "Asset ref block"
+ :asset/edit-block "Edit block"
:asset/confirm-delete "Are you sure you want to delete this {1}?"
:asset/physical-delete "Remove the file too (notice it can't be restored)"
:color/gray "Gray"
diff --git a/src/test/frontend/test/frontend_node_test_runner.cljs b/src/test/frontend/test/frontend_node_test_runner.cljs
index 40f1add6c5..ea973cddb5 100644
--- a/src/test/frontend/test/frontend_node_test_runner.cljs
+++ b/src/test/frontend/test/frontend_node_test_runner.cljs
@@ -5,7 +5,7 @@
[shadow.test.env :as env]
[lambdaisland.glogi.console :as glogi-console]
;; activate humane test output for all tests
- [pjstadig.humane-test-output]))
+ #_[pjstadig.humane-test-output]))
;; Needed for new test runners
(defn ^:dev/after-load reset-test-data! []