diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index afa10f553c5..e6a26f5207d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -83,6 +83,9 @@ jobs: - name: Run some ClojureScript tests against DB version run: DB_GRAPH=1 node static/tests.js -r frontend.db.query-dsl-test + - name: Run ClojureScript query tests against DB version with basic query type + run: DB_GRAPH=1 DB_QUERY_TYPE=basic node static/tests.js -r frontend.db.query-dsl-test + - name: Run ClojureScript tests run: node static/tests.js -e fix-me diff --git a/deps/db/.carve/config.edn b/deps/db/.carve/config.edn index a2922f04f0c..ffc15d10674 100644 --- a/deps/db/.carve/config.edn +++ b/deps/db/.carve/config.edn @@ -13,5 +13,6 @@ logseq.db.frontend.schema logseq.db.frontend.validate logseq.db.test.helper - logseq.db] + logseq.db + logseq.db.frontend.property.type] :report {:format :ignore}} diff --git a/deps/db/bb.edn b/deps/db/bb.edn index c9a9f982412..000c6db732f 100644 --- a/deps/db/bb.edn +++ b/deps/db/bb.edn @@ -31,9 +31,15 @@ (concat (mapcat val rules/rules) ;; TODO: Update linter to handle false positive on ?str-val for :property (rules/extract-rules (dissoc rules/query-dsl-rules :property)) - ;; TODO: Update linter to handle false positive on :task, :priority, :property and :private-property - (rules/extract-rules (dissoc rules/db-query-dsl-rules :task :priority :property :private-property)))))}} + ;; TODO: Update linter to handle false positive on :task, :priority, :*property* rules + (rules/extract-rules (dissoc rules/db-query-dsl-rules + :task :priority + :property :simple-query-property :private-property + :property-scalar-default-value + :property-missing-value + :has-property-or-default-value)))))}} :tasks/config {:large-vars - {:max-lines-count 50}}} + {:max-lines-count 50 + :metadata-exceptions #{:large-vars/doc-var}}}} diff --git a/deps/db/src/logseq/db/frontend/entity_plus.cljc b/deps/db/src/logseq/db/frontend/entity_plus.cljc index e68a6a2e4d3..ff0fc0d5c8b 100644 --- a/deps/db/src/logseq/db/frontend/entity_plus.cljc +++ b/deps/db/src/logseq/db/frontend/entity_plus.cljc @@ -38,50 +38,65 @@ result)] (or result' default-value)))))) +(defn- lookup-kv-with-default-value + [db ^Entity e k default-value] + (or + ;; from kv + (get (.-kv e) k) + ;; from db + (let [result (lookup-entity e k default-value)] + (if (some? result) + result + ;; property default value + (when (qualified-keyword? k) + (when-let [property (d/entity db k)] + (let [schema (lookup-entity property :block/schema nil)] + (if (= :checkbox (:type schema)) + (lookup-entity property :logseq.property/scalar-default-value nil) + (lookup-entity property :logseq.property/default-value nil))))))))) + (defn lookup-kv-then-entity ([e k] (lookup-kv-then-entity e k nil)) ([^Entity e k default-value] (try (when k - (case k - :block/raw-title - (let [db (.-db e)] + (let [db (.-db e)] + (case k + :block/raw-title (if (and (db-based-graph? db) (= "journal" (:block/type e))) (get-journal-title db e) - (lookup-entity e :block/title default-value))) + (lookup-entity e :block/title default-value)) - :block/properties - (let [db (.-db e)] + :block/properties (if (db-based-graph? db) (lookup-entity e :block/properties (->> (into {} e) (filter (fn [[k _]] (db-property/property? k))) (into {}))) - (lookup-entity e :block/properties nil))) - - ;; cache :block/title - :block/title - (or (:block.temp/cached-title @(.-cache e)) - (let [title (get-block-title e k default-value)] - (vreset! (.-cache e) (assoc @(.-cache e) - :block.temp/cached-title title)) - title)) - - :block/_parent - (->> (lookup-entity e k default-value) - (remove (fn [e] (or (:logseq.property/created-from-property e) - (:block/closed-value-property e)))) - seq) - - :block/_raw-parent - (lookup-entity e :block/_parent default-value) - - :property/closed-values - (->> (lookup-entity e :block/_closed-value-property default-value) - (sort-by :block/order)) - - (or (get (.-kv e) k) - (lookup-entity e k default-value)))) + (lookup-entity e :block/properties nil)) + + ;; cache :block/title + :block/title + (or (:block.temp/cached-title @(.-cache e)) + (let [title (get-block-title e k default-value)] + (vreset! (.-cache e) (assoc @(.-cache e) + :block.temp/cached-title title)) + title)) + + :block/_parent + (->> (lookup-entity e k default-value) + (remove (fn [e] (or (:logseq.property/created-from-property e) + (:block/closed-value-property e)))) + seq) + + :block/_raw-parent + (lookup-entity e :block/_parent default-value) + + :property/closed-values + (->> (lookup-entity e :block/_closed-value-property default-value) + (sort-by :block/order)) + + (lookup-kv-with-default-value db e k default-value)))) (catch :default e (js/console.error e))))) diff --git a/deps/db/src/logseq/db/frontend/property.cljs b/deps/db/src/logseq/db/frontend/property.cljs index d36b9847d9f..492324be3b1 100644 --- a/deps/db/src/logseq/db/frontend/property.cljs +++ b/deps/db/src/logseq/db/frontend/property.cljs @@ -147,10 +147,16 @@ :public? true :view-context :page} :queryable? true} - ;; :logseq.property/default-value {:title "Default value" - ;; :schema {:type :any - ;; :public? true - ;; :view-context :property}} + :logseq.property/default-value {:title "Default value" + :schema {:type :entity + :public? false + :hide? true + :view-context :property}} + :logseq.property/scalar-default-value {:title "Non ref type default value" + :schema {:type :any + :public? false + :hide? true + :view-context :property}} :logseq.property.class/properties {:title "Tag Properties" :schema {:type :property :cardinality :many diff --git a/deps/db/src/logseq/db/frontend/property/build.cljs b/deps/db/src/logseq/db/frontend/property/build.cljs index 4147a173c8f..4a4b260967d 100644 --- a/deps/db/src/logseq/db/frontend/property/build.cljs +++ b/deps/db/src/logseq/db/frontend/property/build.cljs @@ -6,25 +6,27 @@ [logseq.db.frontend.property.type :as db-property-type])) (defn- closed-value-new-block - [block-id value property] + [block-id block-type value property] (let [property-id (:db/ident property)] (merge {:block/type "closed value" :block/format :markdown :block/uuid block-id :block/page property-id :block/closed-value-property property-id - :logseq.property/created-from-property property-id + :logseq.property/created-from-property (if (= property-id :logseq.property/default-value) + [:block/uuid block-id] + property-id) :block/parent property-id} - (if (db-property-type/original-value-ref-property-types (get-in property [:block/schema :type])) + (if (db-property-type/property-value-content? block-type property) {:property.value/content value} {:block/title value})))) (defn build-closed-value-block "Builds a closed value block to be transacted" - [block-uuid block-value property {:keys [db-ident icon]}] + [block-uuid block-type block-value property {:keys [db-ident icon]}] (assert block-uuid (uuid? block-uuid)) (cond-> - (closed-value-new-block block-uuid block-value property) + (closed-value-new-block block-uuid block-type block-value property) (and db-ident (keyword? db-ident)) (assoc :db/ident db-ident) @@ -36,10 +38,11 @@ (defn closed-values->blocks [property] - (map (fn [{uuid' :uuid :keys [db-ident value icon]}] + (map (fn [{uuid' :uuid :keys [db-ident value icon schema]}] (cond-> (build-closed-value-block uuid' + (:type schema) value property {:db-ident db-ident :icon icon}) @@ -71,9 +74,11 @@ ;; page block (:db/id block)) :block/parent (:db/id block) - :logseq.property/created-from-property (or (:db/id property) {:db/ident (:db/ident property)}) + :logseq.property/created-from-property (if (= (:db/ident property) :logseq.property/default-value) + (:db/id block) + (or (:db/id property) {:db/ident (:db/ident property)})) :block/order (db-order/gen-key)} - (if (db-property-type/original-value-ref-property-types (get-in property [:block/schema :type])) + (if (db-property-type/property-value-content? (get-in block [:block/schema :type]) property) {:property.value/content value} {:block/title value})) sqlite-util/block-with-timestamps)) diff --git a/deps/db/src/logseq/db/frontend/property/type.cljs b/deps/db/src/logseq/db/frontend/property/type.cljs index f65d1ade2b4..e0d13f11bce 100644 --- a/deps/db/src/logseq/db/frontend/property/type.cljs +++ b/deps/db/src/logseq/db/frontend/property/type.cljs @@ -28,6 +28,14 @@ "Valid property types that can change cardinality" #{:default :number :url :date :node}) +(def default-value-ref-property-types + "Valid ref property :type for default value support" + #{:default :number :checkbox}) + +(def text-ref-property-types + "Valid ref property :types that support text" + #{:default :url :entity}) + (assert (set/subset? cardinality-property-types (set user-built-in-property-types)) "All closed value types are valid property types") @@ -188,3 +196,11 @@ (url? val) :url (contains? #{true false} val) :checkbox :else :default)) + +(defn property-value-content? + "Whether property value should be stored in :property.value/content" + [block-type property] + (or + (original-value-ref-property-types (get-in property [:block/schema :type])) + (and (= (:db/ident property) :logseq.property/default-value) + (original-value-ref-property-types block-type)))) diff --git a/deps/db/src/logseq/db/frontend/rules.cljc b/deps/db/src/logseq/db/frontend/rules.cljc index 42e35b663f1..006ec8ea451 100644 --- a/deps/db/src/logseq/db/frontend/rules.cljc +++ b/deps/db/src/logseq/db/frontend/rules.cljc @@ -155,29 +155,100 @@ (dissoc query-dsl-rules :namespace :page-property :has-page-property :page-tags :all-page-tags) - {:tags - '[(tags ?b ?tags) - [?b :block/tags ?t] - [?t :block/name ?tag] - [(missing? $ ?b :block/link)] - [(contains? ?tags ?tag)]] + {:existing-property-value + '[;; non-ref value + [(existing-property-value ?b ?prop ?val) + [?prop-e :db/ident ?prop] + [(missing? $ ?prop-e :db/valueType)] + [?b ?prop ?val]] + ;; ref value + [(existing-property-value ?b ?prop ?val) + [?prop-e :db/ident ?prop] + [?prop-e :db/valueType :db.type/ref] + [?b ?prop ?pv] + (or [?pv :block/title ?val] + [?pv :property.value/content ?val])]] - :has-property - '[(has-property ?b ?prop) - [?b ?prop _] + :property-missing-value + '[(property-missing-value ?b ?prop-e ?default-p ?default-v) + [?t :logseq.property.class/properties ?prop-e] + [?prop-e :db/ident ?prop] + (object-has-class-property? ?b ?prop) + ;; Notice: `(missing? )` doesn't work here because `de/entity` + ;; returns the default value if there's no value yet. + [(get-else $ ?b ?prop "N/A") ?prop-v] + [(= ?prop-v "N/A")] + [?prop-e ?default-p ?default-v]] + + :property-scalar-default-value + '[(property-scalar-default-value ?b ?prop-e ?default-p ?val) + (property-missing-value ?b ?prop-e ?default-p ?default-v) + [(missing? $ ?prop-e :db/valueType)] + [?prop-e ?default-p ?val]] + + :property-default-value + '[(property-default-value ?b ?prop-e ?default-p ?val) + (property-missing-value ?b ?prop-e ?default-p ?default-v) + (or + [?default-v :block/title ?val] + [?default-v :property.value/content ?val])] + + :property-value + '[[(property-value ?b ?prop-e ?val) + [?prop-e :db/ident ?prop] + (existing-property-value ?b ?prop ?val)] + [(property-value ?b ?prop-e ?val) + (or + (and + [(missing? $ ?prop-e :db/valueType)] + (property-scalar-default-value ?b ?prop-e :logseq.property/scalar-default-value ?val)) + (and + [?prop-e :db/valueType :db.type/ref] + (property-default-value ?b ?prop-e :logseq.property/default-value ?val)))]] + + :object-has-class-property + '[(object-has-class-property? ?b ?prop) + [?prop-e :db/ident ?prop] + [?t :logseq.property.class/properties ?prop-e] + [?b :block/tags ?t]] + + :has-property-or-default-value + '[(has-property-or-default-value? ?b ?prop) + [?prop-e :db/ident ?prop] + (or + [?b ?prop _] + (and (object-has-class-property? ?b ?prop) + (or [?prop-e :logseq.property/default-value _] + [?prop-e :logseq.property/scalar-default-value _])))] + + ;; Checks if a property exists for simple queries. Supports default values + :has-simple-query-property + '[(has-simple-query-property ?b ?prop) [?prop-e :db/ident ?prop] [?prop-e :block/type "property"] + (has-property-or-default-value? ?b ?prop) [?prop-e :block/schema ?prop-schema] [(get ?prop-schema :public? true) ?public] [(= true ?public)]] - ;; Same as has-property except it returns public and private properties like :block/title - :has-private-property - '[(has-private-property ?b ?prop) + ;; Same as has-simple-query-property except it returns public and private properties like :block/title + :has-private-simple-query-property + '[(has-private-simple-query-property ?b ?prop) + [?prop-e :db/ident ?prop] + [?prop-e :block/type "property"] + (has-property-or-default-value? ?b ?prop)] + + ;; Checks if a property exists for any features that are not simple queries + :has-property + '[(has-property ?b ?prop) [?b ?prop _] [?prop-e :db/ident ?prop] - [?prop-e :block/type "property"]] + [?prop-e :block/type "property"] + [?prop-e :block/schema ?prop-schema] + [(get ?prop-schema :public? true) ?public] + [(= true ?public)]] + ;; Checks if a property has a value for any features that are not simple queries :property '[(property ?b ?prop ?val) [?prop-e :db/ident ?prop] @@ -197,22 +268,30 @@ (or [?pv :block/title ?val] [?pv :property.value/content ?val])))] + ;; Checks if a property has a value for simple queries. Supports default values + :simple-query-property + '[(simple-query-property ?b ?prop ?val) + [?prop-e :db/ident ?prop] + [?prop-e :block/type "property"] + [?prop-e :block/schema ?prop-schema] + [(get ?prop-schema :public? true) ?public] + [(get ?prop-schema :type) ?type] + [(= true ?public)] + (property-value ?b ?prop-e ?val)] + ;; Same as property except it returns public and private properties like :block/title - :private-property - '[(private-property ?b ?prop ?val) + :private-simple-query-property + '[(private-simple-query-property ?b ?prop ?val) [?prop-e :db/ident ?prop] [?prop-e :block/type "property"] - [?b ?prop ?pv] - (or - ;; non-ref value - (and - [(missing? $ ?prop-e :db/valueType)] - [?b ?prop ?val]) - ;; ref value - (and - [?prop-e :db/valueType :db.type/ref] - (or [?pv :block/title ?val] - [?pv :property.value/content ?val])))] + (property-value ?b ?prop-e ?val)] + + :tags + '[(tags ?b ?tags) + [?b :block/tags ?t] + [?t :block/name ?tag] + [(missing? $ ?b :block/link)] + [(contains? ?tags ?tag)]] :task '[(task ?b ?statuses) @@ -231,7 +310,25 @@ becomes long or brittle, we could do scan rules for their deps with something like find-rules-in-where" {:task #{:property} - :priority #{:property}}) + :priority #{:property} + :property-missing-value #{:object-has-class-property} + :has-property-or-default-value #{:object-has-class-property} + :has-simple-query-property #{:has-property-or-default-value} + :has-private-simple-query-property #{:has-property-or-default-value} + :property-default-value #{:existing-property-value :property-missing-value} + :property-scalar-default-value #{:existing-property-value :property-missing-value} + :property-value #{:property-default-value :property-scalar-default-value} + :simple-query-property #{:property-value} + :private-simple-query-property #{:property-value}}) + +(defn- get-full-deps + [deps rules-deps] + (loop [deps' deps + result #{}] + (if (seq deps') + (recur (mapcat rules-deps deps') + (into result deps')) + result))) (defn extract-rules "Given a rules map and the rule names to extract, returns a vector of rules to @@ -241,9 +338,9 @@ No dependencies are detected by default though we could add it later e.g. find-rules-in-where" ([rules-m] (extract-rules rules-m (keys rules-m))) ([rules-m rules' & {:keys [deps]}] - (let [rules-with-deps (concat rules' - (when (map? deps) - (mapcat deps rules')))] + (let [rules-with-deps (if (map? deps) + (get-full-deps rules' deps) + rules')] (vec (mapcat #(let [val (rules-m %)] ;; if vector?, rule has multiple clauses diff --git a/deps/db/src/logseq/db/frontend/schema.cljs b/deps/db/src/logseq/db/frontend/schema.cljs index f7a06069a54..22fc3d23893 100644 --- a/deps/db/src/logseq/db/frontend/schema.cljs +++ b/deps/db/src/logseq/db/frontend/schema.cljs @@ -2,7 +2,7 @@ "Main datascript schemas for the Logseq app" (:require [clojure.set :as set])) -(def version 47) +(def version 48) ;; A page is a special block, a page can corresponds to multiple files with the same ":block/name". (def ^:large-vars/data-var schema diff --git a/deps/db/src/logseq/db/sqlite/build.cljs b/deps/db/src/logseq/db/sqlite/build.cljs index 98c6d1efdf5..1583737d4e4 100644 --- a/deps/db/src/logseq/db/sqlite/build.cljs +++ b/deps/db/src/logseq/db/sqlite/build.cljs @@ -83,12 +83,16 @@ (->> properties (keep (fn [[k v]] (if-let [built-in-type (get-in db-property/built-in-properties [k :schema :type])] - (when (and (db-property-type/value-ref-property-types built-in-type) - ;; closed values are referenced by their :db/ident so no need to create values - (not (get-in db-property/built-in-properties [k :closed-values]))) + (if (and (db-property-type/value-ref-property-types built-in-type) + ;; closed values are referenced by their :db/ident so no need to create values + (not (get-in db-property/built-in-properties [k :closed-values]))) (let [property-map {:db/ident k :block/schema {:type built-in-type}}] - [property-map v])) + [property-map v]) + (when-let [built-in-type' (get (:build/properties-ref-types new-block) built-in-type)] + (let [property-map {:db/ident k + :block/schema {:type built-in-type'}}] + [property-map v]))) (when (and (db-property-type/value-ref-property-types (get-in properties-config [k :block/schema :type])) ;; TODO: Support translate-property-value without this hack (not (vector? v))) @@ -159,7 +163,8 @@ {:block-uuid (:block/uuid prop-m) :title (:block/title prop-m)}) {:db/id (or (property-db-ids prop-name) - (throw (ex-info "No :db/id for property" {:property prop-name})))}) + (throw (ex-info "No :db/id for property" {:property prop-name})))} + (select-keys prop-m [:build/properties-ref-types])) pvalue-tx-m (->property-value-tx-m new-block (:build/properties prop-m) properties all-idents)] (cond-> [] (seq pvalue-tx-m) @@ -248,6 +253,8 @@ [:block/schema [:map [:type :keyword]]] [:build/properties {:optional true} User-properties] + [:build/properties-ref-types {:optional true} + [:map-of :keyword :keyword]] [:build/closed-values {:optional true} [:vector [:map @@ -543,7 +550,7 @@ classes-tx pages-and-blocks-tx)))) -(defn build-blocks-tx +(defn ^:large-vars/doc-var build-blocks-tx "Given an EDN map for defining pages, blocks and properties, this creates a map with two keys of transactable data for use with d/transact!. The :init-tx key must be transacted first and the :block-props-tx can be transacted after. @@ -572,6 +579,8 @@ * :build/properties - Define properties on a property page. * :build/closed-values - Define closed values with a vec of maps. A map contains keys :uuid, :value and :icon. * :build/schema-classes - Vec of class name keywords. Defines a property's range classes + * :build/properties-ref-types - Map of internal ref types to public ref types that are valid only for this property. + Useful when remapping value ref types e.g. for :logseq.property/default-value * :classes - This is a map to configure classes where the keys are class name keywords and the values are maps of datascript attributes e.g. `{:block/title \"Foo\"}`. Additional keys available: diff --git a/deps/db/test/logseq/db/frontend/rules_test.cljs b/deps/db/test/logseq/db/frontend/rules_test.cljs index e38c6b2b64e..35317cb1dbc 100644 --- a/deps/db/test/logseq/db/frontend/rules_test.cljs +++ b/deps/db/test/logseq/db/frontend/rules_test.cljs @@ -1,5 +1,5 @@ (ns logseq.db.frontend.rules-test - (:require [cljs.test :refer [deftest is testing]] + (:require [cljs.test :refer [deftest is testing are]] [datascript.core :as d] [logseq.db.frontend.rules :as rules] [logseq.db.test.helper :as db-test])) @@ -10,6 +10,24 @@ db (rules/extract-rules rules/db-query-dsl-rules))) +(deftest get-full-deps + (let [default-value-deps #{:property-default-value + :property-missing-value + :existing-property-value + :object-has-class-property} + property-value-deps (conj default-value-deps :property-value :property-scalar-default-value) + property-deps (conj property-value-deps :simple-query-property) + task-deps #{:property :task} + priority-deps #{:property :priority} + task-priority-deps #{:property :task :priority}] + (are [x y] (= (#'rules/get-full-deps x rules/rules-dependencies) y) + [:property-default-value] default-value-deps + [:property-value] property-value-deps + [:simple-query-property] property-deps + [:task] task-deps + [:priority] priority-deps + [:task :priority] task-priority-deps))) + (deftest has-property-rule (let [conn (db-test/create-conn-with-blocks {:properties {:foo {:block/schema {:type :default}} @@ -107,8 +125,8 @@ [:user.property/number-many 5] [:user.property/foo "bar"] [:user.property/page-many "Page A"]} - (->> (q-with-rules '[:find ?p ?v - :where (property ?b ?p ?v) [?b :block/title "Page"]] + (->> (q-with-rules '[:find ?p ?val + :where (property ?b ?p ?val) [?b :block/title "Page"]] @conn) set)) "property can bind to property and property value args") 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 3899a8f9673..422537c122f 100644 --- a/deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs +++ b/deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs @@ -43,17 +43,23 @@ db) first))) +(defn- extract-rules + [rules] + (rules/extract-rules rules/db-query-dsl-rules + rules + {:deps rules/rules-dependencies})) + (defn- find-block-by-property [db property] (d/q '[:find [(pull ?b [*]) ...] :in $ ?prop % :where (has-property ?b ?prop)] - db property (rules/extract-rules rules/db-query-dsl-rules [:has-property]))) + db property (extract-rules [:has-property]))) (defn- find-block-by-property-value [db property property-value] (->> (d/q '[:find [(pull ?b [*]) ...] :in $ ?prop ?prop-value % :where (property ?b ?prop ?prop-value)] - db property property-value (rules/extract-rules rules/db-query-dsl-rules [:property])) + db property property-value (extract-rules [:property])) first)) (defn- find-page-by-name [db name] diff --git a/deps/outliner/src/logseq/outliner/core.cljs b/deps/outliner/src/logseq/outliner/core.cljs index c3197ca71ee..c828f0dd548 100644 --- a/deps/outliner/src/logseq/outliner/core.cljs +++ b/deps/outliner/src/logseq/outliner/core.cljs @@ -729,15 +729,27 @@ txs-state (ds/new-outliner-txs-state) block-ids (map (fn [b] [:block/uuid (:block/uuid b)]) top-level-blocks) start-block (first top-level-blocks) - end-block (last top-level-blocks)] + end-block (last top-level-blocks) + delete-one-block? (or (= 1 (count top-level-blocks)) (= start-block end-block))] (when (seq top-level-blocks) - (if (or - (= 1 (count top-level-blocks)) - (= start-block end-block)) - (delete-block conn txs-state start-block) - (doseq [id block-ids] - (let [node (d/entity @conn id)] - (otree/-del node txs-state conn))))) + (let [from-property (:logseq.property/created-from-property start-block) + default-value-property? (and (:logseq.property/default-value from-property) + (not= (:db/id start-block) + (:db/id (:logseq.property/default-value from-property))))] + (cond + (and delete-one-block? default-value-property?) + (let [datoms (d/datoms @conn :avet (:db/ident from-property) (:db/id start-block)) + tx-data (map (fn [d] {:db/id (:e d) + (:db/ident from-property) :logseq.property/empty-placeholder}) datoms)] + (when (seq tx-data) (swap! txs-state concat tx-data))) + + delete-one-block? + (delete-block conn txs-state start-block) + + :else + (doseq [id block-ids] + (let [node (d/entity @conn id)] + (otree/-del node txs-state conn)))))) {:tx-data @txs-state})) (defn- move-to-original-position? diff --git a/deps/outliner/src/logseq/outliner/property.cljs b/deps/outliner/src/logseq/outliner/property.cljs index 45523f6f845..abbe0bfd984 100644 --- a/deps/outliner/src/logseq/outliner/property.cljs +++ b/deps/outliner/src/logseq/outliner/property.cljs @@ -74,10 +74,14 @@ :type :error}}))))) (defn ^:api convert-property-input-string - [schema-type v-str] - (if (and (= :number schema-type) (string? v-str)) - (fail-parse-double v-str) - v-str)) + [block-type property v-str] + (let [schema-type (get-in property [:block/schema :type])] + (if (and (or (= :number schema-type) + (and (= (:db/ident property) :logseq.property/default-value) + (= :number block-type))) + (string? v-str)) + (fail-parse-double v-str) + v-str))) (defn- update-datascript-schema [property {type' :type :keys [cardinality]}] @@ -207,7 +211,8 @@ (let [property (d/entity @conn property-id) block (when block-id (d/entity @conn block-id)) _ (assert (some? property) (str "Property " property-id " doesn't exist yet")) - value' (convert-property-input-string (get-in property [:block/schema :type]) value) + value' (convert-property-input-string (get-in block [:block/schema :type]) + property value) new-value-block (cond-> (db-property-build/build-property-value-block (or block property) property value') new-block-id (assoc :block/uuid new-block-id))] @@ -284,7 +289,7 @@ (let [property (d/entity @conn property-id) _ (assert (some? property) (str "Property " property-id " doesn't exist yet")) property-type (get-in property [:block/schema :type] :default) - new-value (if (db-property-type/user-ref-property-types property-type) + new-value (if (db-property-type/all-ref-property-types property-type) (convert-ref-property-value conn property-id v property-type) v) existing-value (get block property-id)] @@ -355,8 +360,18 @@ [conn eid property-id] (throw-error-if-read-only-property property-id) (let [eid (->eid eid) - block (d/entity @conn eid)] + block (d/entity @conn eid) + property (d/entity @conn property-id)] (cond + (= :logseq.property/empty-placeholder (:db/ident (get block property-id))) + nil + + (= (:logseq.property/default-value property) (get block property-id)) + (ldb/transact! conn + [{:db/id (:db/id block) + property-id :logseq.property/empty-placeholder}] + {:outliner-op :save-block}) + (and (ldb/class? block) (= property-id :logseq.property/parent)) (ldb/transact! conn [[:db/add (:db/id block) :logseq.property/parent :logseq.class/Root]] @@ -414,17 +429,17 @@ :classes-properties all-properties})) (defn- property-with-position? - [db property-id block-properties position] + [db property-id block position] (let [property (d/entity db property-id) schema (:block/schema property)] (and (= (:position schema) position) (not (and (:logseq.property/hide-empty-value property) - (nil? (get block-properties property-id)))) + (nil? (get block property-id)))) (not (get-in property [:block/schema :hide?])) (not (and (= (:position schema) :block-below) - (nil? (get block-properties property-id))))))) + (nil? (get block property-id))))))) (defn property-with-other-position? [property] @@ -438,7 +453,7 @@ (->> (:classes-properties (get-block-classes-properties db eid)) (map :db/ident) (concat own-properties) - (filter (fn [id] (property-with-position? db id (:block/properties block) position))) + (filter (fn [id] (property-with-position? db id block position))) (distinct) (map #(d/entity db %)) (ldb/sort-by-order) @@ -455,13 +470,13 @@ (merge {:block/uuid id :block/closed-value-property (:db/id property)} - (if (db-property-type/original-value-ref-property-types (get-in property [:block/schema :type])) + (if (db-property-type/property-value-content? (get-in block [:block/schema :type]) property) {:property.value/content resolved-value} {:block/title resolved-value}))) icon (assoc :logseq.property/icon icon))] (let [max-order (:block/order (last (:property/closed-values property))) - new-block (-> (db-property-build/build-closed-value-block block-id resolved-value + new-block (-> (db-property-build/build-closed-value-block block-id nil resolved-value property {:icon icon}) (assoc :block/order (db-order/gen-key max-order nil)))] [new-block @@ -482,7 +497,7 @@ property-type (get property-schema :type :default)] (when (contains? db-property-type/closed-value-property-types property-type) (let [value' (if (string? value) (string/trim value) value) - resolved-value (convert-property-input-string (:type property-schema) value') + resolved-value (convert-property-input-string nil property value') validate-message (validate-property-value-aux (get-property-value-schema @conn property-type property {:new-closed-value? true}) resolved-value diff --git a/deps/outliner/test/logseq/outliner/property_test.cljs b/deps/outliner/test/logseq/outliner/property_test.cljs index f01e4f4c6c7..172daf05419 100644 --- a/deps/outliner/test/logseq/outliner/property_test.cljs +++ b/deps/outliner/test/logseq/outliner/property_test.cljs @@ -39,7 +39,7 @@ (let [test-uuid (random-uuid)] (are [x y] (= (let [[schema-type value] x] - (outliner-property/convert-property-input-string schema-type value)) y) + (outliner-property/convert-property-input-string nil {:block/schema {:type schema-type}} value)) y) [:number "1"] 1 [:number "1.2"] 1.2 [:url test-uuid] test-uuid diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index c9db62ca86e..e53c0f520c5 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -2770,7 +2770,10 @@ (.preventDefault e) :else - (block-content-on-pointer-down e block block-id content edit-input-id config))))))] + (let [f (:on-block-content-pointer-down config)] + (if (fn? f) + (f e) + (block-content-on-pointer-down e block block-id content edit-input-id config))))))))] [:div.block-content.inline (cond-> {:id (str "block-content-" uuid) :key (str "block-content-" uuid)} @@ -3369,7 +3372,14 @@ (when order-list? " is-order-list") (when (string/blank? title) " is-blank") (when original-block " embed-block")) - :haschild (str (boolean has-child?))} + :haschild (str (boolean has-child?)) + :on-focus (fn [] + (when (:property-default-value? config) + (when-let [f (:on-block-content-pointer-down config)] + (f))))} + + (:property-default-value? config) + (assoc :data-is-property-default-value (:property-default-value? config)) original-block (assoc :originalblockid (str (:block/uuid original-block))) diff --git a/src/main/frontend/components/container.cljs b/src/main/frontend/components/container.cljs index 16a2235db6b..b4a8cecd6de 100644 --- a/src/main/frontend/components/container.cljs +++ b/src/main/frontend/components/container.cljs @@ -836,11 +836,13 @@ ;; block bullet (and block-id (parse-uuid block-id)) - (let [block (.closest target ".ls-block")] + (let [block (.closest target ".ls-block") + property-default-value? (when block + (= "true" (d/attr block "data-is-property-default-value")))] (when block (state/clear-selection!) (state/conj-selection-block! block :down)) - (show! (cp-content/block-context-menu-content target (uuid block-id)))) + (show! (cp-content/block-context-menu-content target (uuid block-id) property-default-value?))) :else false)] diff --git a/src/main/frontend/components/content.cljs b/src/main/frontend/components/content.cljs index e1c631309e0..a337f9a1a9a 100644 --- a/src/main/frontend/components/content.cljs +++ b/src/main/frontend/components/content.cljs @@ -64,7 +64,7 @@ {:key "delete" :on-click #(do (editor-handler/delete-selection %) (state/hide-custom-context-menu!) - (shui/popup-hide!))} + (shui/popup-hide!))} (t :editor/delete-selection) (shui/dropdown-menu-shortcut (ui/keyboard-shortcut-from-config :editor/delete))) @@ -165,15 +165,15 @@ (p/let [exists? (page-handler/ [:div.px-4.py-2.text-sm {:on-click (fn [e] (util/stop e))} @@ -183,7 +183,7 @@ :on-key-down (fn [e] (util/stop-propagation e) (when (and (= "Enter" (util/ekey e)) - (not (string/blank? (util/trim-safe @input)))) + (not (string/blank? (util/trim-safe @input)))) (submit!))) :on-change (fn [e] (reset! input (util/evalue e)))}] @@ -192,7 +192,7 @@ (ui/button (t :submit) :on-click submit!)] (shui/dropdown-menu-separator)]) (shui/dropdown-menu-item - {:key "Make a Template" + {:key "Make a Template" :on-click (fn [e] (util/stop e) (reset! edit? true))} @@ -200,7 +200,7 @@ (rum/defc ^:large-vars/cleanup-todo block-context-menu-content < shortcut/disable-all-shortcuts - [_target block-id] + [_target block-id property-default-value?] (let [repo (state/get-current-repo) db? (config/db-based-graph? repo)] (when-let [block (db/entity [:block/uuid block-id])] @@ -265,18 +265,20 @@ #(export/export-blocks [block-id] {:whiteboard? false})))} (t :content/copy-export-as)) - (shui/dropdown-menu-item - {:key "Cut" - :on-click (fn [_e] - (editor-handler/cut-block! block-id))} - (t :editor/cut) - (shui/dropdown-menu-shortcut (ui/keyboard-shortcut-from-config :editor/cut))) + (when-not property-default-value? + (shui/dropdown-menu-item + {:key "Cut" + :on-click (fn [_e] + (editor-handler/cut-block! block-id))} + (t :editor/cut) + (shui/dropdown-menu-shortcut (ui/keyboard-shortcut-from-config :editor/cut)))) - (shui/dropdown-menu-item - {:key "delete" - :on-click #(editor-handler/delete-block-aux! block)} - (t :editor/delete-selection) - (shui/dropdown-menu-shortcut (ui/keyboard-shortcut-from-config :editor/delete))) + (when-not property-default-value? + (shui/dropdown-menu-item + {:key "delete" + :on-click #(editor-handler/delete-block-aux! block)} + (t :editor/delete-selection) + (shui/dropdown-menu-shortcut (ui/keyboard-shortcut-from-config :editor/delete)))) (shui/dropdown-menu-separator) diff --git a/src/main/frontend/components/icon.cljs b/src/main/frontend/components/icon.cljs index 1b3e8a44855..699d5e61ade 100644 --- a/src/main/frontend/components/icon.cljs +++ b/src/main/frontend/components/icon.cljs @@ -437,7 +437,7 @@ (shui/tabler-icon "trash" {:size 17})))]]])) (rum/defc icon-picker - [icon-value {:keys [empty-label disabled? initial-open? del-btn? on-chosen icon-props popup-opts]}] + [icon-value {:keys [empty-label disabled? initial-open? del-btn? on-chosen icon-props popup-opts button-opts]}] (let [*trigger-ref (rum/use-ref nil) content-fn (if config/publishing? @@ -458,19 +458,22 @@ ;; trigger (let [has-icon? (some? icon-value)] (shui/button - {:ref *trigger-ref - :variant (if has-icon? :ghost :text) - :size :sm - :class (if has-icon? "px-1 leading-none" "font-normal text-sm px-[0.5px] opacity-50") - :on-click (fn [^js e] - (when-not disabled? - (shui/popup-show! (.-target e) content-fn - (medley/deep-merge - {:align :start - :id :ls-icon-picker - :content-props {:class "ls-icon-picker" - :onEscapeKeyDown #(.preventDefault %)}} - popup-opts))))} + (merge + {:ref *trigger-ref + :variant :ghost + :size :sm + :class (if has-icon? "px-1 leading-none text-muted-foreground hover:text-foreground" + "font-normal text-sm px-[0.5px] text-muted-foreground hover:text-foreground") + :on-click (fn [^js e] + (when-not disabled? + (shui/popup-show! (.-target e) content-fn + (medley/deep-merge + {:align :start + :id :ls-icon-picker + :content-props {:class "ls-icon-picker" + :onEscapeKeyDown #(.preventDefault %)}} + popup-opts))))} + button-opts) (if has-icon? (if (vector? icon-value) ; hiccup icon-value diff --git a/src/main/frontend/components/property.cljs b/src/main/frontend/components/property.cljs index eff5563299f..27889c7ac39 100644 --- a/src/main/frontend/components/property.cljs +++ b/src/main/frontend/components/property.cljs @@ -111,7 +111,10 @@ (and block (= type :checkbox)) (p/do! (ui/hide-popups-until-preview-popup!) - (pv/ strong { @apply text-sm px-0.5 flex-grow cursor-pointer font-normal - select-none active:opacity-60; + select-none; } > .ui__button { - @apply !p-0 !w-7 !h-7 overflow-hidden flex-shrink-0; + @apply !p-0 !w-5 !h-5 overflow-hidden flex-shrink-0 text-muted-foreground hover:text-foreground; } - > a.del { - @apply items-center absolute right-0 top-0 px-1 py-[7px] scale-90 hidden - opacity-80 hover:opacity-100 active:opacity-80 text-gray-08 hover:text-red-700; - } - - &:hover a.del { - @apply flex; - } - - &:hover .ls-icon-grip-vertical { - @apply opacity-80; + > .del { + @apply hover:text-red-700; } } } diff --git a/src/main/frontend/components/property/config.cljs b/src/main/frontend/components/property/config.cljs index a8dc9291d24..82fb3e2d4d5 100644 --- a/src/main/frontend/components/property/config.cljs +++ b/src/main/frontend/components/property/config.cljs @@ -23,6 +23,7 @@ [rum.core :as rum] [frontend.db-mixins :as db-mixins] [frontend.components.property.value :as pv] + [frontend.components.property.default-value :as pdv] [frontend.components.select :as select] [frontend.db.model :as model] [frontend.handler.db-based.page :as db-page-handler] @@ -250,7 +251,7 @@ #(some-> (gdom/getElement id) (.focus)) 32)) (rum/defc dropdown-editor-menuitem - [{:keys [id icon title desc submenu-content item-props sub-content-props disabled? toggle-checked? on-toggle-checked-change]}] + [{:keys [id icon title desc submenu-content item-props sub-content-props disabled? toggle-checked? on-toggle-checked-change checkbox?]}] (let [submenu-content (when-not disabled? submenu-content) item-props' (if (and disabled? (:on-select item-props)) (assoc item-props :on-select (fn [] nil)) @@ -283,22 +284,26 @@ :id id1} item-props') %))] (wrap-menuitem - [:div.inner-wrap + [:div.inner-wrap.cursor-pointer {:class (util/classnames [{:disabled disabled?}])} [:strong (some-> icon (name) (shui/tabler-icon {:size 14 :style {:margin-top "-1"}})) [:span title]] - (if (fn? desc) (desc) - (if (boolean? toggle-checked?) - [:span.scale-90.flex.items-center - (shui/switch {:id id2 :size "sm" :checked toggle-checked? - :disabled disabled? :on-click #(util/stop-propagation %) - :on-checked-change (or on-toggle-checked-change identity)})] - [:label [:span desc] - (when disabled? (shui/tabler-icon "forbid-2" {:size 15}))]))]))) - -(rum/defc choice-item-content + (cond + (fn? desc) + (desc) + (boolean? toggle-checked?) + [:span.scale-90.flex.items-center + (let [f (if checkbox? shui/checkbox shui/switch)] + (f {:id id2 :size "sm" :checked toggle-checked? + :disabled disabled? :on-click #(util/stop-propagation %) + :on-checked-change (or on-toggle-checked-change identity)}))] + :else + [:label [:span desc] + (when disabled? (shui/tabler-icon "forbid-2" {:size 15}))])]))) + +(rum/defc choice-item-content < rum/reactive db-mixins/query [property block] (let [delete-choice! (fn [] (p/do! @@ -312,28 +317,49 @@ value (db-property/closed-value-content block)] [:li - (shui/tabler-icon "grip-vertical" {:size 14}) - (shui/button {:size "sm" :variant :outline} - (icon-component/icon-picker icon {:on-chosen (fn [_e icon] (update-icon! icon)) - :popup-opts {:align "start"} - :del-btn? (boolean icon) - :empty-label "?"})) + (let [property-type (get-in property [:block/schema :type]) + property (db/sub-block (:db/id property)) + default-type? (contains? #{:default :number} property-type) + default-value (when default-type? (:logseq.property/default-value property)) + default-value? (= (:db/id default-value) (:db/id block))] + (when default-type? + (shui/checkbox {:size :sm + :title "Set as default choice" + :class "opacity-50 hover:opacity-100" + :checked default-value? + :on-checked-change (fn [] + (let [property-ident :logseq.property/default-value] + (if default-value? + (db-property-handler/remove-block-property! (:db/ident property) property-ident) + (db-property-handler/set-block-property! (:db/ident property) property-ident (:db/id block)))))}))) + + (shui/button {:size :sm :variant :ghost :title "Drag && Drop to reorder"} + (shui/tabler-icon "grip-vertical" {:size 14})) + (icon-component/icon-picker icon {:on-chosen (fn [_e icon] (update-icon! icon)) + :popup-opts {:align "start"} + :del-btn? (boolean icon) + :empty-label "?" + :button-opts {:title "Set Icon"}}) [:strong {:on-click (fn [^js e] (shui/popup-show! (.-target e) (fn [] (choice-base-edit-form property block)) {:id :ls-base-edit-form :align "start"}))} value] - [:a.del {:on-click delete-choice! - :title "Delete this choice"} - (shui/tabler-icon "x" {:size 16})]])) + + (shui/button + {:size :sm :variant :ghost :class "del" + :title "Delete this choice" + :on-click delete-choice!} + (shui/tabler-icon "x" {:size 16}))])) (rum/defc add-existing-values [property values {:keys [toggle-fn]}] (let [uuid-values? (every? uuid? values) values' (if uuid-values? (let [values' (map #(db/entity [:block/uuid %]) values)] - (->> (util/distinct-by db-property/closed-value-content values') + (->> values' + (util/distinct-by db-property/closed-value-content) (map :block/uuid))) values)] [:div.flex.flex-col.gap-1.w-64.p-4.overflow-y-auto @@ -385,6 +411,8 @@ {:db/id (:db/id property)})] {:outliner-op :save-block})))})]) + (shui/dropdown-menu-separator) + ;; add choice (when-not disabled? (dropdown-editor-menuitem @@ -400,10 +428,14 @@ (shui/popup-show! (.-target e) (fn [{:keys [id]}] (let [opts {:toggle-fn (fn [] (shui/popup-hide! id))} - values' (->> (if (contains? db-property-type/user-ref-property-types (get-in property [:block/schema :type])) - (map #(:block/uuid (db/entity %)) values) - values) - (remove string/blank?) + values' (->> (if (contains? db-property-type/all-ref-property-types (get-in property [:block/schema :type])) + (->> values + (map db/entity) + (remove (fn [e] + (let [value (db-property/property-value-content e)] + (and (string? value) (string/blank? value))))) + (map :block/uuid)) + (remove string/blank? values)) distinct)] (if (seq values') (add-existing-values property values' opts) @@ -486,6 +518,23 @@ :item-props (assoc item-props :data-value value)}] (dropdown-editor-menuitem option)))])) +(rum/defc default-value-subitem + [property] + (let [property-type (get-in property [:block/schema :type]) + option (if (= :checkbox property-type) + (let [default-value (:logseq.property/scalar-default-value property)] + {:icon :settings-2 + :title "Default value" + :toggle-checked? (boolean default-value) + :checkbox? true + :on-toggle-checked-change (fn [] + (db-property-handler/set-block-property! (:block/uuid property) :logseq.property/scalar-default-value (not default-value)))}) + (let [default-value (:logseq.property/default-value property)] + {:icon :settings-2 :title "Default value" + :desc (if default-value (db-property/property-value-content default-value) "Set value") + :submenu-content (fn [] (pdv/default-value-config property))}))] + (dropdown-editor-menuitem option))) + (rum/defc ^:large-vars/cleanup-todo dropdown-editor-impl "property: block entity" [property owner-block values {:keys [class-schema? debug?]}] @@ -527,26 +576,32 @@ [:div.px-4 (class-select property {:default-open? false})])})) + (when (and (contains? db-property-type/default-value-ref-property-types property-type) + (not (db-property/many? property)) + (not (and enable-closed-values? + (seq (:property/closed-values property))))) + (default-value-subitem property)) + (when enable-closed-values? (let [values (:property/closed-values property)] (dropdown-editor-menuitem {:icon :list :title "Available choices" :desc (when (seq values) (str (count values) " choices")) :submenu-content (fn [] (choices-sub-pane property {:disabled? config/publishing?}))}))) - (let [many? (db-property/many? property)] - (dropdown-editor-menuitem {:icon :checks :title "Multiple values" - :toggle-checked? many? - :disabled? (or disabled? (not (contains? db-property-type/cardinality-property-types property-type))) - :on-toggle-checked-change - (fn [] - (let [update-cardinality-fn #(db-property-handler/upsert-property! (:db/ident property) - (assoc property-schema :cardinality (if many? :one :many)) {})] + (when (and (contains? db-property-type/cardinality-property-types property-type) (not disabled?)) + (let [many? (db-property/many? property)] + (dropdown-editor-menuitem {:icon :checks :title "Multiple values" + :toggle-checked? many? + :on-toggle-checked-change + (fn [] + (let [update-cardinality-fn #(db-property-handler/upsert-property! (:db/ident property) + (assoc property-schema :cardinality (if many? :one :many)) {})] ;; Only show dialog for existing values as it can be reversed for unused properties - (if (and (seq values) (not many?)) - (-> (shui/dialog-confirm! - "This action cannot be undone. Do you want to change this property to have multiple values?") - (p/then update-cardinality-fn)) - (update-cardinality-fn))))})) + (if (and (seq values) (not many?)) + (-> (shui/dialog-confirm! + "This action cannot be undone. Do you want to change this property to have multiple values?") + (p/then update-cardinality-fn)) + (update-cardinality-fn))))}))) (let [property-type (get-in property [:block/schema :type]) group' (->> [(when (and (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property))) diff --git a/src/main/frontend/components/property/default_value.cljs b/src/main/frontend/components/property/default_value.cljs new file mode 100644 index 00000000000..19d7a4640d8 --- /dev/null +++ b/src/main/frontend/components/property/default_value.cljs @@ -0,0 +1,11 @@ +(ns frontend.components.property.default-value + (:require [rum.core :as rum] + [frontend.components.property.value :as pv] + [frontend.db :as db])) + +(rum/defc default-value-config + [property] + (let [default-value (:logseq.property/default-value property)] + (pv/property-value property + (db/entity :logseq.property/default-value) + default-value {}))) diff --git a/src/main/frontend/components/property/value.cljs b/src/main/frontend/components/property/value.cljs index 9651293ce85..87d54c591e9 100644 --- a/src/main/frontend/components/property/value.cljs +++ b/src/main/frontend/components/property/value.cljs @@ -103,30 +103,38 @@ :on-chosen on-chosen!})])) (defn- select-type? - [property type] + [block property type] (or (contains? #{:node :number :date :page :class :property} type) - ;; closed values - (seq (:property/closed-values property)))) + ;; closed values + (seq (:property/closed-values property)) + (and (= (:db/ident property) :logseq.property/default-value) + (= (get-in block [:block/schema :type]) :number)))) (defn > (d/q '[:find [?objects ...] - :in $ ?prop - :where [?objects ?prop]] + (->> (d/q '[:find [?b ...] + :in $ % ?prop + :where + (has-property-or-default-value? ?b ?prop)] (conn/get-db repo) + (rules/extract-rules rules/db-query-dsl-rules [:has-property-or-default-value] + {:deps rules/rules-dependencies}) (:db/ident property)) (map #(db-utils/entity repo %))))) diff --git a/src/main/frontend/db/query_dsl.cljs b/src/main/frontend/db/query_dsl.cljs index 2d2cabeca16..4a1017919be 100644 --- a/src/main/frontend/db/query_dsl.cljs +++ b/src/main/frontend/db/query_dsl.cljs @@ -334,18 +334,24 @@ (let [k (if db-graph? (->db-keyword-property (nth e 1)) (->file-keyword-property (nth e 1))) v (nth e 2) v' (if db-graph? (->db-property-value k v) (->file-property-value v))] - (if private-property? - {:query (list 'private-property '?b k v') - :rules [:private-property]} + (if db-graph? + (if private-property? + {:query (list 'private-simple-query-property '?b k v') + :rules [:private-simple-query-property]} + {:query (list 'simple-query-property '?b k v') + :rules [:simple-query-property]}) {:query (list 'property '?b k v') :rules [:property]}))) (defn- build-property-one-arg [e {:keys [db-graph? private-property?]}] (let [k (if db-graph? (->db-keyword-property (nth e 1)) (->file-keyword-property (nth e 1)))] - (if private-property? - {:query (list 'has-private-property '?b k) - :rules [:has-private-property]} + (if db-graph? + (if private-property? + {:query (list 'has-private-simple-query-property '?b k) + :rules [:has-private-simple-query-property]} + {:query (list 'has-simple-query-property '?b k) + :rules [:has-simple-query-property]}) {:query (list 'has-property '?b k) :rules [:has-property]}))) diff --git a/src/main/frontend/worker/db/migrate.cljs b/src/main/frontend/worker/db/migrate.cljs index 1792abdc508..fc1a0b3ec97 100644 --- a/src/main/frontend/worker/db/migrate.cljs +++ b/src/main/frontend/worker/db/migrate.cljs @@ -147,41 +147,41 @@ (defn- update-hl-color-and-page [conn _search-db] (when (ldb/db-based-graph? @conn) - (let [db @conn - hl-color (d/entity db :logseq.property.pdf/hl-color) - hl-page (d/entity db :logseq.property.pdf/hl-page) - existing-colors (d/datoms db :avet :logseq.property.pdf/hl-color) - color-update-tx (mapcat + (let [db @conn + hl-color (d/entity db :logseq.property.pdf/hl-color) + hl-page (d/entity db :logseq.property.pdf/hl-page) + existing-colors (d/datoms db :avet :logseq.property.pdf/hl-color) + color-update-tx (mapcat + (fn [datom] + (let [block (d/entity db (:v datom)) + color-ident (keyword "logseq.property" (str "color." (:block/title block)))] + (if block + [[:db/add (:e datom) :logseq.property.pdf/hl-color color-ident] + [:db/retractEntity (:db/id block)]] + [[:db/retract (:e datom) :logseq.property.pdf/hl-color]]))) + existing-colors) + page-datoms (d/datoms db :avet :logseq.property.pdf/hl-page) + page-update-tx (mapcat (fn [datom] (let [block (d/entity db (:v datom)) - color-ident (keyword "logseq.property" (str "color." (:block/title block)))] - (if block - [[:db/add (:e datom) :logseq.property.pdf/hl-color color-ident] + value (db-property/property-value-content block)] + (if (integer? value) + [[:db/add (:e datom) :logseq.property.pdf/hl-page value] [:db/retractEntity (:db/id block)]] - [[:db/retract (:e datom) :logseq.property.pdf/hl-color]]))) - existing-colors) - page-datoms (d/datoms db :avet :logseq.property.pdf/hl-page) - page-update-tx (mapcat - (fn [datom] - (let [block (d/entity db (:v datom)) - value (db-property/property-value-content block)] - (if (integer? value) - [[:db/add (:e datom) :logseq.property.pdf/hl-page value] - [:db/retractEntity (:db/id block)]] - [[:db/retract (:e datom) :logseq.property.pdf/hl-page]]))) - page-datoms)] + [[:db/retract (:e datom) :logseq.property.pdf/hl-page]]))) + page-datoms)] ;; update schema first - (d/transact! conn - (concat - [{:db/ident :logseq.property.pdf/hl-page - :block/schema {:type :raw-number}} - [:db/retract (:db/id hl-page) :db/valueType] - {:db/ident :logseq.property.pdf/hl-color - :block/schema {:type :default}}] - (db-property-build/closed-values->blocks - (assoc hl-color :closed-values (get-in db-property/built-in-properties [:logseq.property.pdf/hl-color :closed-values]))))) + (d/transact! conn + (concat + [{:db/ident :logseq.property.pdf/hl-page + :block/schema {:type :raw-number}} + [:db/retract (:db/id hl-page) :db/valueType] + {:db/ident :logseq.property.pdf/hl-color + :block/schema {:type :default}}] + (db-property-build/closed-values->blocks + (assoc hl-color :closed-values (get-in db-property/built-in-properties [:logseq.property.pdf/hl-color :closed-values]))))) ;; migrate data - (concat color-update-tx page-update-tx)))) + (concat color-update-tx page-update-tx)))) (defn- store-url-value-in-block-title [conn _search-db] @@ -338,6 +338,7 @@ m (cond-> (db-property-build/build-closed-value-block uuid' + nil "Card View" property {:db-ident :logseq.property.view/type.card}) @@ -459,7 +460,8 @@ :block/title :block/closed-value-property :block/created-at :block/updated-at :logseq.property.attribute/property-schema-classes :logseq.property.attribute/property-value-content]}] - [47 {:fix replace-hidden-type-with-schema}]]) + [47 {:fix replace-hidden-type-with-schema}] + [48 {:properties [:logseq.property/default-value :logseq.property/scalar-default-value]}]]) (let [max-schema-version (apply max (map first schema-version->updates))] (assert (<= db-schema/version max-schema-version)) diff --git a/src/test/frontend/db/query_dsl_test.cljs b/src/test/frontend/db/query_dsl_test.cljs index 416cffa87dd..861418413ed 100644 --- a/src/test/frontend/db/query_dsl_test.cljs +++ b/src/test/frontend/db/query_dsl_test.cljs @@ -19,9 +19,12 @@ ;; ============ (def dsl-query* - "When $EXAMPLE set, prints query result of build query. Useful for - documenting examples and debugging" - (if (some? js/process.env.EXAMPLE) + "Overrides dsl-query/query with ENV variables. When $EXAMPLE is set, prints query + result of build query. This is useful for documenting examples and debugging. + When $DB_QUERY_TYPE is set, runs query tests against other versions of simple query e.g. + more basic property rules" + (cond + (some? js/process.env.EXAMPLE) (fn dsl-query-star [& args] (let [old-build-query query-dsl/build-query] (with-redefs [query-dsl/build-query @@ -30,6 +33,24 @@ (println "EXAMPLE:" (pr-str (:query res))) res))] (apply query-dsl/query args)))) + (some? js/process.env.DB_QUERY_TYPE) + (fn dsl-query-star [& args] + (let [old-build-property @#'query-dsl/build-property] + (with-redefs [query-dsl/build-property + (fn [& args'] + (let [m (apply old-build-property args') + m' (cond + (= (:rules m) [:simple-query-property]) + {:rules [:property] + :query (apply list 'property (rest (:query m)))} + (= (:rules m) [:has-simple-query-property]) + {:rules [:has-property] + :query (apply list 'has-property (rest (:query m)))} + :else + m)] + m'))] + (apply query-dsl/query args)))) + :else query-dsl/query)) (defn- ->smart-query @@ -148,9 +169,9 @@ prop-d:: [[nada]]"}]) (dsl-query "(property prop-d no-space-link)"))) "Blocks have property value with no space") - (is (= ["b3" "b4"] - (map (comp first str/split-lines :block/title) - (dsl-query "(property prop-d)"))) + (is (= #{"b3" "b4"} + (set (map (comp first str/split-lines :block/title) + (dsl-query "(property prop-d)")))) "Blocks that have a property")) (deftest block-property-queries @@ -183,6 +204,60 @@ prop-d:: [[nada]]"}]) (map :block/title (dsl-query "(property \"zzz name!\")"))) "filter can handle property name"))) +(when (and js/process.env.DB_GRAPH (not js/process.env.DB_QUERY_TYPE)) + (deftest property-default-type-default-value-queries + (load-test-files-for-db-graph + {:properties + {:default {:block/schema {:type :default} + :build/properties + {:logseq.property/default-value "foo"} + :build/properties-ref-types {:entity :number}}} + :classes {:Class1 {:build/schema-properties [:default]}} + :pages-and-blocks + [{:page {:block/title "page1"} + :blocks [{:block/title "b1" + :build/properties {:default "foo"}} + {:block/title "b2" + :build/properties {:default "bar"}} + {:block/title "b3" + :build/tags [:Class1]}]}]}) + + (is (= ["b3" "b2" "b1"] + (map :block/title (dsl-query "(property :user.property/default)"))) + "Blocks with any :default property or tagged with a tag that has that default-value property") + (is (= ["b1" "b3"] + (map :block/title (dsl-query "(property :user.property/default \"foo\")"))) + "Blocks with :default property value or tagged with a tag that has that default-value property value") + (is (= ["b2"] + (map :block/title (dsl-query "(property :user.property/default \"bar\")"))) + "Blocks with :default property value and not tagged with a tag that has that default-value property value")) + + (deftest property-checkbox-type-default-value-queries + (load-test-files-for-db-graph + {:properties + {:checkbox {:block/schema {:type :checkbox} + :build/properties + {:logseq.property/scalar-default-value true}}} + :classes {:Class1 {:build/schema-properties [:checkbox]}} + :pages-and-blocks + [{:page {:block/title "page1"} + :blocks [{:block/title "b1" + :build/properties {:checkbox true}} + {:block/title "b2" + :build/properties {:checkbox false}} + {:block/title "b3" + :build/tags [:Class1]}]}]}) + + (is (= ["b3" "b2" "b1"] + (map :block/title (dsl-query "(property :user.property/checkbox)"))) + "Blocks with any :checkbox property or tagged with a tag that has that default-value property") + (is (= ["b1" "b3"] + (map :block/title (dsl-query "(property :user.property/checkbox true)"))) + "Blocks with :checkbox property value or tagged with a tag that has that default-value property value") + (is (= ["b2"] + (map :block/title (dsl-query "(property :user.property/checkbox false)"))) + "Blocks with :checkbox property value and not tagged with a tag that has that default-value property value"))) + (deftest block-property-query-performance (let [pages (->> (repeat 10 {:tags ["tag1" "tag2"]}) (map-indexed (fn [idx {:keys [tags]}] @@ -264,10 +339,10 @@ prop-d:: [[nada]]"}]) "Boolean false"))) (when-not js/process.env.DB_GRAPH - (deftest page-property-queries - (testing "page property tests with default config" - (test-helper/with-config {} - (page-property-queries-test))))) + (deftest page-property-queries + (testing "page property tests with default config" + (test-helper/with-config {} + (page-property-queries-test))))) (deftest task-queries (load-test-files [{:file/path "pages/page1.md"