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! []