Browse Source

Refactor/move property handler to outliner dep (#11311)

The end goal is to get rid of `db/transact!` and send outliner ops to
the db worker.

Currently, some property ops are async, set-block-property! will also
need to be async because when setting a non-ref value (e.g. a number
str "2"), we need to query whether a block with the value exists, this
unfortunately, will be an async query, so we're risking turning more
functions to async in the future which makes it hard to reason about
the implementation.
Tienson Qin 1 year ago
parent
commit
ce4cad2cc7
31 changed files with 1235 additions and 1028 deletions
  1. 10 0
      deps/common/src/logseq/common/util.cljs
  2. 4 1
      deps/db/src/logseq/db/frontend/malli_schema.cljs
  3. 6 0
      deps/db/src/logseq/db/frontend/property.cljs
  4. 6 8
      deps/db/src/logseq/db/frontend/property/build.cljs
  5. 21 16
      deps/db/src/logseq/db/frontend/property/type.cljs
  6. 6 8
      deps/db/src/logseq/db/sqlite/util.cljs
  7. 6 5
      deps/graph-parser/src/logseq/graph_parser/block.cljs
  8. 3 3
      deps/outliner/src/logseq/outliner/core.cljs
  9. 109 2
      deps/outliner/src/logseq/outliner/op.cljs
  10. 587 0
      deps/outliner/src/logseq/outliner/property.cljs
  11. 3 3
      src/main/frontend/components/block.cljs
  12. 3 3
      src/main/frontend/components/db_based/page.cljs
  13. 1 3
      src/main/frontend/components/page.cljs
  14. 11 14
      src/main/frontend/components/property.cljs
  15. 16 18
      src/main/frontend/components/property/closed_value.cljs
  16. 1 3
      src/main/frontend/components/property/util.cljs
  17. 34 35
      src/main/frontend/components/property/value.cljs
  18. 17 1
      src/main/frontend/db/async.cljs
  19. 1 1
      src/main/frontend/db_worker.cljs
  20. 75 590
      src/main/frontend/handler/db_based/property.cljs
  21. 0 7
      src/main/frontend/handler/db_based/property/util.cljs
  22. 19 19
      src/main/frontend/handler/editor.cljs
  23. 7 7
      src/main/frontend/handler/property.cljs
  24. 0 6
      src/main/frontend/handler/property/util.cljs
  25. 90 45
      src/main/frontend/modules/outliner/op.cljs
  26. 4 3
      src/main/frontend/search.cljs
  27. 0 10
      src/main/frontend/util.cljc
  28. 21 15
      src/test/frontend/db/db_based_model_test.cljs
  29. 0 61
      src/test/frontend/handler/db_based/property_async_test.cljs
  30. 0 87
      src/test/frontend/handler/db_based/property_closed_value_test.cljs
  31. 174 54
      src/test/frontend/handler/db_based/property_test.cljs

+ 10 - 0
deps/common/src/logseq/common/util.cljs

@@ -60,6 +60,16 @@
   (when (string? tag-name)
     (not (re-find #"[#\t\r\n]+" tag-name))))
 
+(defn tag?
+  "Whether `s` is a tag."
+  [s]
+  (and (string? s)
+       (string/starts-with? s "#")
+       (or
+        (not (string/includes? s " "))
+        (string/starts-with? s "#[[")
+        (string/ends-with? s "]]"))))
+
 (defn safe-subs
   ([s start]
    (let [c (count s)]

+ 4 - 1
deps/db/src/logseq/db/frontend/malli_schema.cljs

@@ -68,7 +68,10 @@
   [db validate-fn [{:block/keys [schema] :as property} property-val] & {:keys [new-closed-value?]}]
   ;; For debugging
   ;; (when (not= "logseq.property" (namespace (:db/ident property))) (prn :validate-val (dissoc property :property/closed-values) property-val))
-  (let [validate-fn' (if (db-property-type/property-types-with-db (:type schema)) (partial validate-fn db) validate-fn)
+  (let [validate-fn' (if (db-property-type/property-types-with-db (:type schema))
+                       (fn [value]
+                         (validate-fn db value {:new-closed-value? new-closed-value?}))
+                       validate-fn)
         validate-fn'' (if (and (db-property-type/closed-value-property-types (:type schema))
                                ;; new closed values aren't associated with the property yet
                                (not new-closed-value?)

+ 6 - 0
deps/db/src/logseq/db/frontend/property.cljs

@@ -310,3 +310,9 @@
 (defn many?
   [property]
   (= (:db/cardinality property) :db.cardinality/many))
+
+(defn property-value-when-closed
+  "Returns property value if the given entity is type 'closed value' or nil"
+  [ent]
+  (when (contains? (:block/type ent) "closed value")
+    (:block/content ent)))

+ 6 - 8
deps/db/src/logseq/db/frontend/property/build.cljs

@@ -5,8 +5,6 @@
             [logseq.db.frontend.order :as db-order]
             [datascript.core :as d]))
 
-(defonce hidden-page-name-prefix "$$$")
-
 (defn- closed-value-new-block
   [block-id value property]
   (let [property-id (:db/ident property)]
@@ -43,7 +41,7 @@
 (defn build-closed-values
   "Builds all the tx needed for property with closed values including
    the hidden page and closed value blocks as needed"
-  [db-ident prop-name property {:keys [property-attributes from-ui-thread?]}]
+  [db-ident prop-name property {:keys [property-attributes]}]
   (let [property-schema (:block/schema property)
         property-tx (merge (sqlite-util/build-new-property db-ident property-schema {:original-name prop-name
                                                                                      :ref-type? true})
@@ -62,14 +60,14 @@
                          value
                          property
                          {:db-ident db-ident :icon icon :description description})
-                         (not from-ui-thread?)
+                         true
                          (assoc :block/order (db-order/gen-key))))
                      (:closed-values property))]
             closed-value-blocks-tx))]
     (into [property-tx] hidden-tx)))
 
 (defn build-property-value-block
-  [block property value parse-block]
+  [block property value]
   (-> {:block/uuid (d/squuid)
        :block/format :markdown
        :block/content value
@@ -79,6 +77,6 @@
                      (:db/id block))
        :block/parent (:db/id block)
        :logseq.property/created-from-property (or (:db/id property)
-                                                  {:db/ident (:db/ident property)})}
-      sqlite-util/block-with-timestamps
-      parse-block))
+                                                  {:db/ident (:db/ident property)})
+       :block/order (db-order/gen-key)}
+      sqlite-util/block-with-timestamps))

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

@@ -26,7 +26,7 @@
 
 (def value-ref-property-types
   "Property value ref types"
-  #{:default :url :number})
+  #{:default :url :number :template})
 
 (def ref-property-types
   "User facing ref types"
@@ -73,9 +73,25 @@
   (some? (d/entity db id)))
 
 (defn- url-entity?
-  [db val]
-  (when-let [ent (d/entity db val)]
-    (url? (:block/content ent))))
+  [db val {:keys [new-closed-value?]}]
+  (if new-closed-value?
+    (url? val)
+    (when-let [ent (d/entity db val)]
+      (url? (:block/content ent)))))
+
+(defn- string-entity?
+  [db id-or-value _opts]
+  (or (string? id-or-value)
+    (when-let [entity (d/entity db id-or-value)]
+      (string? (:block/content entity)))))
+
+(defn- number-entity?
+  [db id-or-value {:keys [new-closed-value?]}]
+  (if new-closed-value?
+    (number? id-or-value)
+    (when-let [entity (d/entity db id-or-value)]
+      (number? (some-> (:block/content entity)
+                       parse-double)))))
 
 (defn- property-value-block?
   [db s]
@@ -94,17 +110,6 @@
     (and (some? (:block/original-name ent))
          (contains? (:block/type ent) "journal"))))
 
-(defn- string-or-closed-string?
-  [db s]
-  (or (string? s)
-      (when-let [entity (d/entity db s)]
-        (string? (:block/content entity)))))
-
-(defn- number-entity?
-  [db id]
-  (when-let [entity (d/entity db id)]
-    (number? (some-> (:block/content entity)
-                     parse-double))))
 
 (def built-in-validation-schemas
   "Map of types to malli validation schemas that validate a property value for that type"
@@ -113,7 +118,7 @@
               property-value-block?]
    :string   [:fn
               {:error/message "should be a string"}
-              string-or-closed-string?]
+              string-entity?]
    :number   [:fn
               {:error/message "should be a number"}
               number-entity?]

+ 6 - 8
deps/db/src/logseq/db/sqlite/util.cljs

@@ -74,17 +74,16 @@
   "Build a standard new property so that it is is consistent across contexts. Takes
    an optional map with following keys:
    * :original-name - Case sensitive property name. Defaults to deriving this from db-ident
-   * :block-uuid - :block/uuid for property
-   * :from-ui-thread? - whether calls from the UI thread"
+   * :block-uuid - :block/uuid for property"
   ([db-ident prop-schema] (build-new-property db-ident prop-schema {}))
-  ([db-ident prop-schema {:keys [original-name block-uuid ref-type? from-ui-thread?]}]
+  ([db-ident prop-schema {:keys [original-name block-uuid ref-type?]}]
    (assert (keyword? db-ident))
    (let [db-ident' (if (qualified-keyword? db-ident)
                      db-ident
                      (db-property/create-user-property-ident-from-name (name db-ident)))
          prop-name (or original-name (name db-ident'))
-         block-order (when-not from-ui-thread? (db-order/gen-key nil))
-         classes (:classes prop-schema)]
+         classes (:classes prop-schema)
+         prop-schema (assoc prop-schema :type (get prop-schema :type :default))]
      (block-with-timestamps
       (cond->
        {:db/ident db-ident'
@@ -97,9 +96,8 @@
         :db/index true
         :db/cardinality (if (= :many (:cardinality prop-schema))
                           :db.cardinality/many
-                          :db.cardinality/one)}
-        block-order
-        (assoc :block/order block-order)
+                          :db.cardinality/one)
+        :block/order (db-order/gen-key)}
         (seq classes)
         (assoc :property/schema.classes classes)
         (or ref-type? (contains? (conj db-property-type/ref-property-types :entity) (:type prop-schema)))

+ 6 - 5
deps/graph-parser/src/logseq/graph_parser/block.cljs

@@ -283,11 +283,12 @@
   [original-page-name date-formatter]
   (when original-page-name
     (let [page-name (common-util/page-name-sanity-lc original-page-name)
-          day (date-time-util/journal-title->int page-name (date-time-util/safe-journal-title-formatters date-formatter))]
-     (if day
-       (let [original-page-name (date-time-util/int->journal-title day date-formatter)]
-         [original-page-name (common-util/page-name-sanity-lc original-page-name) day])
-       [original-page-name page-name day]))))
+          day (when date-formatter
+                (date-time-util/journal-title->int page-name (date-time-util/safe-journal-title-formatters date-formatter)))]
+      (if day
+        (let [original-page-name (date-time-util/int->journal-title day date-formatter)]
+          [original-page-name (common-util/page-name-sanity-lc original-page-name) day])
+        [original-page-name page-name day]))))
 
 (def convert-page-if-journal (memoize convert-page-if-journal-impl))
 

+ 3 - 3
deps/outliner/src/logseq/outliner/core.cljs

@@ -688,7 +688,7 @@
             page-blocks)))
 
 (defn ^:api delete-block
-  [_repo conn txs-state node {:keys [_date-formatter]}]
+  [conn txs-state node {:keys [_date-formatter]}]
   (otree/-del node txs-state conn)
   @txs-state)
 
@@ -704,7 +704,7 @@
 (defn ^:api ^:large-vars/cleanup-todo delete-blocks
   "Delete blocks from the tree.
   `blocks` need to be sorted by left&parent(from top to bottom)"
-  [repo conn date-formatter blocks delete-opts]
+  [_repo conn date-formatter blocks delete-opts]
   [:pre [(seq blocks)]]
   (let [top-level-blocks (filter-top-level-blocks @conn blocks)
         non-consecutive? (and (> (count top-level-blocks) 1) (seq (ldb/get-non-consecutive-blocks @conn top-level-blocks)))
@@ -716,7 +716,7 @@
     (if (or
          (= 1 (count top-level-blocks))
          (= start-block end-block))
-      (delete-block repo conn txs-state start-block (assoc delete-opts :date-formatter date-formatter))
+      (delete-block conn txs-state start-block (assoc delete-opts :date-formatter date-formatter))
       (doseq [id block-ids]
         (let [node (d/entity @conn id)]
           (otree/-del node txs-state conn))))

+ 109 - 2
deps/outliner/src/logseq/outliner/op.cljs

@@ -2,11 +2,13 @@
   "Transact outliner ops"
   (:require [logseq.outliner.transaction :as outliner-tx]
             [logseq.outliner.core :as outliner-core]
+            [logseq.outliner.property :as outliner-property]
             [datascript.core :as d]
             [malli.core :as m]))
 
 (def ^:private op-schema
   [:multi {:dispatch first}
+   ;; blocks
    [:save-block
     [:catn
      [:op :keyword]
@@ -30,11 +32,73 @@
    [:indent-outdent-blocks
     [:catn
      [:op :keyword]
-     [:args [:tuple ::ids :boolean ::option]]]]])
+     [:args [:tuple ::ids :boolean ::option]]]]
+
+   ;; properties
+   [:upsert-property
+    [:catn
+     [:op :keyword]
+     [:args [:tuple ::property-id ::schema ::option]]]]
+   [:set-block-property
+    [:catn
+     [:op :keyword]
+     [:args [:tuple ::block-id ::property-id ::value]]]]
+   [:remove-block-property
+    [:catn
+     [:op :keyword]
+     [:args [:tuple ::block-id ::property-id]]]]
+   [:delete-property-value
+    [:catn
+     [:op :keyword]
+     [:args [:tuple ::block-id ::property-id ::value]]]]
+   [:create-property-text-block
+    [:catn
+     [:op :keyword]
+     [:args [:tuple ::block-id ::property-id ::value ::option]]]]
+   [:collapse-expand-block-property
+    [:catn
+     [:op :keyword]
+     [:args [:tuple ::block-id ::property-id :boolean]]]]
+   [:batch-set-property
+    [:catn
+     [:op :keyword]
+     [:args [:tuple ::block-ids ::property-id ::value]]]]
+   [:batch-remove-property
+    [:catn
+     [:op :keyword]
+     [:args [:tuple ::block-ids ::property-id]]]]
+   [:class-add-property
+    [:catn
+     [:op :keyword]
+     [:args [:tuple ::class-id ::property-id]]]]
+   [:class-remove-property
+    [:catn
+     [:op :keyword]
+     [:args [:tuple ::class-id ::property-id]]]]
+   [:upsert-closed-value
+    [:catn
+     [:op :keyword]
+     [:args [:tuple ::property-id ::option]]]]
+   [:delete-closed-value
+    [:catn
+     [:op :keyword]
+     [:args [:tuple ::property-id ::value]]]]
+   [:add-existing-values-to-closed-values
+    [:catn
+     [:op :keyword]
+     [:args [:tuple ::property-id ::values]]]]])
 
 (def ^:private ops-schema
   [:schema {:registry {::id int?
                        ::block map?
+                       ::schema map?
+                       ;; FIXME: use eid integer
+                       ::block-id :any
+                       ::block-ids [:sequential ::block-id]
+                       ::class-id int?
+                       ::property-id [:or int? keyword? nil?]
+                       ::value :any
+                       ::values [:sequential ::value]
                        ::option [:maybe map?]
                        ::blocks [:sequential ::block]
                        ::ids [:sequential ::id]}}
@@ -44,6 +108,7 @@
 
 (defn apply-ops!
   [repo conn ops date-formatter opts]
+  ;; (prn :debug :outliner-ops ops)
   (assert (ops-validator ops) ops)
   (let [opts' (assoc opts
                      :transact-opts {:conn conn}
@@ -53,6 +118,7 @@
      opts'
      (doseq [[op args] ops]
        (case op
+         ;; blocks
          :save-block
          (apply outliner-core/save-block! repo conn date-formatter args)
 
@@ -84,5 +150,46 @@
          (let [[block-ids indent? opts] args
                blocks (keep #(d/entity @conn %) block-ids)]
            (when (seq blocks)
-             (outliner-core/indent-outdent-blocks! repo conn blocks indent? opts))))))
+             (outliner-core/indent-outdent-blocks! repo conn blocks indent? opts)))
+
+         ;; properties
+         :upsert-property
+         (apply outliner-property/upsert-property! conn args)
+
+         :set-block-property
+         (apply outliner-property/set-block-property! conn args)
+
+         :remove-block-property
+         (apply outliner-property/remove-block-property! conn args)
+
+         :delete-property-value
+         (apply outliner-property/delete-property-value! conn args)
+
+         :create-property-text-block
+         (apply outliner-property/create-property-text-block! conn args)
+
+         :collapse-expand-block-property
+         (apply outliner-property/collapse-expand-block-property! conn args)
+
+         :batch-set-property
+         (apply outliner-property/batch-set-property! conn args)
+
+         :batch-remove-property
+         (apply outliner-property/batch-remove-property! conn args)
+
+         :class-add-property
+         (apply outliner-property/class-add-property! conn args)
+
+         :class-remove-property
+         (apply outliner-property/class-remove-property! conn args)
+
+         :upsert-closed-value
+         (apply outliner-property/upsert-closed-value! conn args)
+
+         :delete-closed-value
+         (apply outliner-property/delete-closed-value! conn args)
+
+         :add-existing-values-to-closed-values
+         (apply outliner-property/add-existing-values-to-closed-values! conn args))))
+
     @*insert-result))

+ 587 - 0
deps/outliner/src/logseq/outliner/property.cljs

@@ -0,0 +1,587 @@
+(ns logseq.outliner.property
+  "Property related operations"
+  (:require [clojure.string :as string]
+            [datascript.core :as d]
+            [logseq.common.util :as common-util]
+            [logseq.common.util.page-ref :as page-ref]
+            [logseq.db :as ldb]
+            [logseq.db.frontend.malli-schema :as db-malli-schema]
+            [logseq.db.frontend.order :as db-order]
+            [logseq.db.frontend.property :as db-property]
+            [logseq.db.frontend.property.build :as db-property-build]
+            [logseq.db.frontend.property.type :as db-property-type]
+            [logseq.db.sqlite.util :as sqlite-util]
+            [logseq.graph-parser.block :as gp-block]
+            [logseq.outliner.core :as outliner-core]
+            [malli.error :as me]
+            [malli.util :as mu]))
+
+;; schema -> type, cardinality, object's class
+;;           min, max -> string length, number range, cardinality size limit
+
+(defn- build-property-value-tx-data
+  ([block property-id value]
+   (build-property-value-tx-data block property-id value (= property-id :logseq.task/status)))
+  ([block property-id value status?]
+   (when (some? value)
+     (let [block (assoc (outliner-core/block-with-updated-at {:db/id (:db/id block)})
+                        property-id value)
+           block-tx-data (cond-> block
+                          status?
+                           (assoc :block/tags :logseq.class/task))]
+       [block-tx-data]))))
+
+(defn- get-property-value-schema
+  "Gets a malli schema to validate the property value for the given property type and builds
+   it with additional args like datascript db"
+  [db property-type property & {:keys [new-closed-value?]
+                             :or {new-closed-value? false}}]
+  (let [property-val-schema (or (get db-property-type/built-in-validation-schemas property-type)
+                                (throw (ex-info (str "No validation for property type " (pr-str property-type)) {})))
+        [schema-opts schema-fn] (if (vector? property-val-schema)
+                                  (rest property-val-schema)
+                                  [{} property-val-schema])]
+    [:fn
+     schema-opts
+     (fn property-value-schema [property-val]
+       (db-malli-schema/validate-property-value db schema-fn [property property-val] {:new-closed-value? new-closed-value?}))]))
+
+(defn- fail-parse-long
+  [v-str]
+  (let [result (parse-long v-str)]
+    (or result
+        (throw (js/Error. (str "Can't convert \"" v-str "\" to a number"))))))
+
+(defn- fail-parse-double
+  [v-str]
+  (let [result (parse-double v-str)]
+    (or result
+        (throw (js/Error. (str "Can't convert \"" v-str "\" to a number"))))))
+
+(defn- infer-schema-from-input-string
+  [v-str]
+  (try
+    (cond
+      (fail-parse-long v-str) :number
+      (fail-parse-double v-str) :number
+      (common-util/url? v-str) :url
+      (contains? #{"true" "false"} (string/lower-case v-str)) :checkbox
+      :else :default)
+    (catch :default _e
+      :default)))
+
+(defn convert-property-input-string
+  [schema-type v-str]
+  (if (and (= :number schema-type) (string? v-str))
+    (fail-parse-double v-str)
+    v-str))
+
+(defn- update-datascript-schema
+  [property {:keys [type cardinality]}]
+  (let [ident (:db/ident property)
+        cardinality (if (= cardinality :many) :db.cardinality/many :db.cardinality/one)
+        new-type (or type (get-in property [:block/schema :type]))]
+    (cond->
+     {:db/ident ident
+      :db/cardinality cardinality}
+      (db-property-type/ref-property-types new-type)
+      (assoc :db/valueType :db.type/ref))))
+
+(defn ensure-unique-db-ident
+  "Ensures the given db-ident is unique. If a db-ident conflicts, it is made
+  unique by adding a suffix with a unique number e.g. :db-ident-1 :db-ident-2"
+  [db db-ident]
+  (if (d/entity db db-ident)
+    (let [existing-idents
+          (d/q '[:find [?ident ...]
+                 :in $ ?ident-name
+                 :where
+                 [?b :db/ident ?ident]
+                 [(str ?ident) ?str-ident]
+                 [(clojure.string/starts-with? ?str-ident ?ident-name)]]
+               db
+               (str db-ident "-"))
+          new-ident (if-let [max-num (->> existing-idents
+                                          (keep #(parse-long (string/replace-first (str %) (str db-ident "-") "")))
+                                          (apply max))]
+                      (keyword (namespace db-ident) (str (name db-ident) "-" (inc max-num)))
+                      (keyword (namespace db-ident) (str (name db-ident) "-1")))]
+      new-ident)
+    db-ident))
+
+(defn upsert-property!
+  "Updates property if property-id is given. Otherwise creates a property
+   with the given property-id or :property-name option. When a property is created
+   it is ensured to have a unique :db/ident"
+  [conn property-id schema {:keys [property-name properties]}]
+  (let [db @conn
+        db-ident (or property-id (db-property/create-user-property-ident-from-name property-name))]
+    (assert (qualified-keyword? db-ident))
+    (if-let [property (and (qualified-keyword? property-id) (d/entity db db-ident))]
+      (let [changed-property-attrs
+            ;; Only update property if something has changed as we are updating a timestamp
+            (cond-> {}
+              (not= schema (:block/schema property))
+              (assoc :block/schema schema)
+              (and (some? property-name) (not= property-name (:block/original-name property)))
+              (assoc :block/original-name property-name))
+            property-tx-data
+            (cond-> []
+              (seq changed-property-attrs)
+              (conj (outliner-core/block-with-updated-at
+                     (merge {:db/ident db-ident}
+                            (common-util/dissoc-in changed-property-attrs [:block/schema :cardinality]))))
+              (or (not= (:type schema) (get-in property [:block/schema :type]))
+                  (and (:cardinality schema) (not= (:cardinality schema) (keyword (name (:db/cardinality property)))))
+                  (and (= :default (:type schema)) (not= :db.type/ref (:db/valueType property)))
+                  (seq (:property/closed-values property)))
+              (conj (update-datascript-schema property schema)))
+            tx-data (concat property-tx-data
+                            (when (seq properties)
+                              (mapcat
+                               (fn [[property-id v]]
+                                 (build-property-value-tx-data property property-id v)) properties)))
+            many->one? (and (db-property/many? property) (= :one (:cardinality schema)))]
+        (when (seq tx-data)
+          (d/transact! conn tx-data {:outliner-op :update-propertyxo
+                                     :property-id (:db/id property)
+                                     :many->one? many->one?})))
+      (let [k-name (or (and property-name (name property-name))
+                       (name property-id))
+            db-ident' (ensure-unique-db-ident @conn db-ident)]
+        (assert (some? k-name)
+                (prn "property-id: " property-id ", property-name: " property-name))
+        (d/transact! conn
+                     [(sqlite-util/build-new-property db-ident' schema {:original-name k-name})]
+                     {:outliner-op :new-property})))))
+
+(defn validate-property-value
+  [schema value]
+  (me/humanize (mu/explain-data schema value)))
+
+(defn page-name->map
+  "Wrapper around logseq.graph-parser.block/page-name->map that adds in db"
+  [db original-page-name with-id?]
+  (gp-block/page-name->map original-page-name with-id? db true nil))
+
+(defn- resolve-tag!
+  "Change `v` to a tag's db id if v is a string tag, e.g. `#book`"
+  [conn v]
+  (when (and (string? v)
+             (common-util/tag? (string/trim v)))
+    (let [tag-without-hash (common-util/safe-subs (string/trim v) 1)
+          tag (or (page-ref/get-page-name tag-without-hash) tag-without-hash)]
+      (when-not (string/blank? tag)
+        (let [db @conn
+              e (ldb/get-case-page db tag)
+              e' (if e
+                   (do
+                     (when-not (contains? (:block/type e) "tag")
+                       (d/transact! conn [{:db/id (:db/id e)
+                                           :block/type (set (conj (:block/type e) "class"))}]))
+                     e)
+                   (let [m (assoc (page-name->map @conn tag true)
+                                  :block/type #{"class"})]
+                     (d/transact! conn [m])
+                     m))]
+          (:db/id e'))))))
+
+(defn- ->eid
+  [id]
+  (if (uuid? id) [:block/uuid id] id))
+
+(declare set-block-property!)
+
+(defn create-property-text-block!
+  "`template-id`: which template the new block belongs to"
+  [conn block-id property-id value {:keys [template-id new-block-id]}]
+  (let [property (d/entity @conn property-id)
+        block (when block-id (d/entity @conn block-id))]
+    (when property
+      (let [new-value-block (cond-> (db-property-build/build-property-value-block (or block property) property value)
+                              true
+                              (assoc
+                               :block.temp/fully-loaded? true)
+                              new-block-id
+                              (assoc :block/uuid new-block-id)
+                              (int? template-id)
+                              (assoc :block/tags template-id
+                                     :logseq.property/created-from-template template-id))]
+        (d/transact! conn [new-value-block] {:outliner-op :insert-blocks})
+        (let [property-id (:db/ident property)]
+          (when (and property-id block)
+            (when-let [block-id (:db/id (d/entity @conn [:block/uuid (:block/uuid new-value-block)]))]
+              (set-block-property! conn (:db/id block) property-id block-id)))
+          (:block/uuid new-value-block))))))
+
+(defn- get-property-value-eid
+  [db property-id raw-value]
+  (first
+   (d/q '[:find [?v ...]
+          :in $ ?property-id ?raw-value
+          :where
+          [?b ?property-id ?v]
+          [?v :block/content ?raw-value]]
+        db
+        property-id
+        raw-value)))
+
+(defn set-block-property!
+  "Updates a block property's value for the an existing property-id."
+  [conn block-eid property-id v]
+  (let [block-eid (->eid block-eid)
+        db @conn
+        _ (assert (qualified-keyword? property-id) "property-id should be a keyword")
+        block (d/entity @conn block-eid)
+        property (d/entity @conn property-id)
+        _ (assert (some? property) (str "Property " property-id " doesn't exists yet"))
+        k-name (:block/original-name property)
+        property-schema (:block/schema property)
+        {:keys [type] :or {type :default}} property-schema
+        v' (or (resolve-tag! conn v) v)
+        db-attribute? (contains? db-property/db-attribute-properties property-id)
+        ref-type? (db-property-type/ref-property-types type)]
+    (cond
+      db-attribute?
+      (d/transact! conn [{:db/id (:db/id block) property-id v'}]
+                   {:outliner-op :save-block})
+
+      :else
+      (let [v' (cond
+                 (= v' :logseq.property/empty-placeholder)
+                 (if (= type :checkbox) false v')
+
+                 ref-type?
+                 (if (and (integer? v')
+                          (or (and (= type :number) (= property-id (:db/ident (:logseq.property/created-from-property (d/entity db v')))))
+                              (not= type :number)))
+                   v'
+                   (or (get-property-value-eid db property-id (str v'))
+                       (let [v-uuid (create-property-text-block! conn nil (:db/id property) (str v') {})]
+                         (:db/id (d/entity @conn [:block/uuid v-uuid])))))
+                 :else
+                 v')
+            v'' (if property v' (or v' ""))]
+        (when (some? v'')
+          (let [infer-schema (infer-schema-from-input-string v'')
+                property-type' (or type infer-schema)
+                schema (get-property-value-schema @conn property-type' (or property
+                                                                           {:block/schema {:type property-type'}}))
+                existing-value (when-let [id (:db/ident property)]
+                                 (get block id))
+                new-value* (if (= v'' :logseq.property/empty-placeholder)
+                             v''
+                             (try
+                               (convert-property-input-string property-type' v'')
+                               (catch :default e
+                                 (js/console.error e)
+                                 ;; (notification/show! (str e) :error false)
+                                 nil)))]
+            (when-not (= existing-value new-value*)
+              (if-let [msg (and
+                            (not= new-value* :logseq.property/empty-placeholder)
+                            (validate-property-value schema
+                                                    ;; normalize :many values for components that only provide single value
+                                                     (if (and (db-property/many? property) (not (coll? new-value*)))
+                                                       #{new-value*}
+                                                       new-value*)))]
+                (let [msg' (str "\"" k-name "\"" " " (if (coll? msg) (first msg) msg))]
+                  ;; (notification/show! msg' :warning)
+                  (prn :debug :msg msg' :property k-name :v new-value*))
+                (let [status? (= :logseq.task/status (:db/ident property))
+                      ;; don't modify maps
+                      new-value (if (or (sequential? new-value*) (set? new-value*))
+                                  (if (= :coll property-type')
+                                    (vec (remove string/blank? new-value*))
+                                    (set (remove string/blank? new-value*)))
+                                  new-value*)
+                      tx-data (build-property-value-tx-data block property-id new-value status?)]
+                  (d/transact! conn tx-data {:outliner-op :save-block}))))))))))
+
+(defn batch-set-property!
+  "Notice that this works only for properties with cardinality equals to `one`."
+  [conn block-ids property-id v]
+  (assert property-id "property-id is nil")
+  (let [block-eids (map ->eid block-ids)
+        property (d/entity @conn property-id)]
+    (when property
+      (let [type (:type (:block/schema property))
+            infer-schema (when-not type (infer-schema-from-input-string v))
+            property-type (or type infer-schema :default)
+            many? (db-property/many? property)
+            status? (= :logseq.task/status (:db/ident property))
+            txs (->>
+                 (mapcat
+                  (fn [eid]
+                    (when-let [block (d/entity @conn eid)]
+                      (when (and (some? v) (not many?))
+                        (when-let [v* (try
+                                        (convert-property-input-string property-type v)
+                                        (catch :default e
+                                          ;; (notification/show! (str e) :error false)
+                                          nil))]
+                          (build-property-value-tx-data block property-id v* status?)))))
+                  block-eids)
+                 (remove nil?))]
+        (when (seq txs)
+          (d/transact! conn txs {:outliner-op :save-block}))))))
+
+(defn batch-remove-property!
+  [conn block-ids property-id]
+  (let [block-eids (map ->eid block-ids)]
+    (when-let [property (d/entity @conn property-id)]
+      (let [txs (mapcat
+                 (fn [eid]
+                   (when-let [block (d/entity @conn eid)]
+                     (let [value (get block property-id)
+                           block-value? (= :default (get-in property [:block/schema :type] :default))
+                           property-block (when block-value? (d/entity @conn (:db/id value)))
+                           retract-blocks-tx (when (and property-block
+                                                        (some? (get property-block :logseq.property/created-from-property)))
+                                               (let [txs-state (atom [])]
+                                                 (outliner-core/delete-block conn txs-state property-block {:children? true})
+                                                 @txs-state))]
+                       (concat
+                        [[:db/retract eid (:db/ident property)]]
+                        retract-blocks-tx))))
+                 block-eids)]
+        (when (seq txs)
+          (d/transact! conn txs {:outliner-op :save-block}))))))
+
+(defn remove-block-property!
+  [conn eid property-id]
+  (let [eid (->eid eid)]
+    (if (contains? db-property/db-attribute-properties property-id)
+      (when-let [block (d/entity @conn eid)]
+        (d/transact! conn
+                     [[:db/retract (:db/id block) property-id]]
+                     {:outliner-op :save-block}))
+      (batch-remove-property! conn [eid] property-id))))
+
+(defn delete-property-value!
+  "Delete value if a property has multiple values"
+  [conn block-eid property-id property-value]
+  (when-let [property (d/entity @conn property-id)]
+    (let [block (d/entity @conn block-eid)]
+      (when (and block (not= property-id (:db/ident block)) (db-property/many? property))
+       (d/transact! conn
+                    [[:db/retract (:db/id block) property-id property-value]]
+                    {:outliner-op :save-block})))))
+
+(defn collapse-expand-block-property!
+  "Notice this works only if the value itself if a block (property type should be either :default or :template)"
+  [conn block-id property-id collapse?]
+  (let [f (if collapse? :db/add :db/retract)]
+    (d/transact! conn
+                 [[f block-id :block/collapsed-properties property-id]]
+                 {:outliner-op :save-block})))
+
+(defn get-class-parents
+  [tags]
+  (let [tags' (filter (fn [tag] (contains? (:block/type tag) "class")) tags)
+        *classes (atom #{})]
+    (doseq [tag tags']
+      (when-let [parent (:class/parent tag)]
+        (loop [current-parent parent]
+          (when (and
+                 current-parent
+                 (contains? (:block/type parent) "class")
+                 (not (contains? @*classes (:db/id parent))))
+            (swap! *classes conj current-parent)
+            (recur (:class/parent current-parent))))))
+    @*classes))
+
+(defn get-block-classes-properties
+  [db eid]
+  (let [block (d/entity db eid)
+        classes (->> (:block/tags block)
+                     (sort-by :block/name)
+                     (filter (fn [tag] (contains? (:block/type tag) "class"))))
+        class-parents (get-class-parents classes)
+        all-classes (->> (concat classes class-parents)
+                         (filter (fn [class]
+                                   (seq (:class/schema.properties class)))))
+        all-properties (-> (mapcat (fn [class]
+                                     (map :db/ident (:class/schema.properties class))) all-classes)
+                           distinct)]
+    {:classes classes
+     :all-classes all-classes           ; block own classes + parent classes
+     :classes-properties all-properties}))
+
+(defn- closed-value-other-position?
+  [db property-id block-properties]
+  (and
+   (some? (get block-properties property-id))
+   (let [schema (:block/schema (d/entity db property-id))]
+     (= (:position schema) "block-beginning"))))
+
+(defn get-block-other-position-properties
+  [db eid]
+  (let [block (d/entity db eid)
+        own-properties (keys (:block/properties block))]
+    (->> (:classes-properties (get-block-classes-properties db eid))
+         (concat own-properties)
+         (filter (fn [id] (closed-value-other-position? db id (:block/properties block))))
+         (distinct))))
+
+(defn block-has-viewable-properties?
+  [block-entity]
+  (let [properties (:block/properties block-entity)]
+    (or
+     (seq (:block/alias block-entity))
+     (and (seq properties)
+          (not= properties [:logseq.property/icon])))))
+
+(defn upsert-closed-value!
+  "id should be a block UUID or nil"
+  [conn property-id {:keys [id value icon description]
+                     :or {description ""}}]
+  (assert (or (nil? id) (uuid? id)))
+  (let [db @conn
+        property (d/entity db property-id)
+        property-schema (:block/schema property)
+        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)
+            closed-values (:property/closed-values property)
+            default-closed-values? (and (= :default property-type) (seq closed-values))
+            value (if (and default-closed-values? (string? value) (not (string/blank? value)))
+                    (let [result (create-property-text-block! conn nil
+                                                              (:db/id property)
+                                                              value
+                                                              {})]
+                      (:db/id (d/entity @conn [:block/uuid (:block-id result)])))
+                    value)
+            resolved-value (try
+                             (convert-property-input-string (:type property-schema) value)
+                             (catch :default e
+                               (js/console.error e)
+                                 ;; (notification/show! (str e) :error false)
+                               nil))
+            block (when id (d/entity @conn [:block/uuid id]))
+            validate-message (validate-property-value
+                              (get-property-value-schema @conn property-type property {:new-closed-value? true})
+                              resolved-value)]
+        (cond
+          (some (fn [b]
+                  (and (= (str resolved-value) (str (or (db-property/property-value-when-closed b)
+                                                        (:block/uuid b))))
+                       (not= id (:block/uuid b)))) closed-values)
+          (do
+            ;; (notification/show! "Choice already exists" :warning)
+            :value-exists)
+
+          validate-message
+          (do
+            ;; (notification/show! validate-message :warning)
+            :value-invalid)
+
+          (nil? resolved-value)
+          nil
+
+          :else
+          (let [block-id (or id (ldb/new-block-id))
+                icon (when-not (and (string? icon) (string/blank? icon)) icon)
+                description (string/trim description)
+                description (when-not (string/blank? description) description)
+                resolved-value (if (= property-type :number) (str resolved-value) resolved-value)
+                tx-data (if block
+                          [(let [schema (:block/schema block)]
+                             (cond->
+                              (outliner-core/block-with-updated-at
+                               {:block/uuid id
+                                :block/content resolved-value
+                                :block/closed-value-property (:db/id property)
+                                :block/schema (if description
+                                                (assoc schema :description description)
+                                                (dissoc schema :description))})
+                               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
+                                                                                          property {:icon icon
+                                                                                                    :description description})
+                                              (assoc :block/order (db-order/gen-key max-order nil)))]
+                            [new-block
+                             (outliner-core/block-with-updated-at
+                              {:db/id (:db/id property)})]))]
+            {:block-id block-id
+             :tx-data tx-data}))))))
+
+(defn add-existing-values-to-closed-values!
+  "Adds existing values as closed values and returns their new block uuids"
+  [conn property-id values]
+  (when-let [property (d/entity @conn property-id)]
+    (when (seq values)
+      (let [values' (remove string/blank? values)]
+        (assert (every? uuid? values') "existing values should all be UUIDs")
+        (let [values (keep #(d/entity @conn [:block/uuid %]) values')]
+          (when (seq values)
+            (let [value-property-tx (map (fn [id]
+                                           {:db/id id
+                                            :block/type "closed value"
+                                            :block/closed-value-property (:db/id property)})
+                                         (map :db/id values))
+                  property-tx (outliner-core/block-with-updated-at {:db/id (:db/id property)})]
+              (d/transact! conn (cons property-tx value-property-tx)
+                           {:outliner-op :save-blocks}))))))))
+
+(defn delete-closed-value!
+  "Returns true when deleted or if not deleted displays warning and returns false"
+  [conn property-id value-block-id]
+  (when-let [value-block (d/entity @conn value-block-id)]
+    (cond
+      (ldb/built-in? value-block)
+      (do
+      ;; (notification/show! "The choice can't be deleted because it's built-in." :warning)
+        false)
+
+      :else
+      (let [tx-data [[:db/retractEntity (:db/id value-block)]
+                     (outliner-core/block-with-updated-at
+                      {:db/id property-id})]]
+        (d/transact! conn tx-data)))))
+
+(defn get-property-block-created-block
+  "Get the root block and property that created this property block."
+  [db eid]
+  (let [block (d/entity db eid)
+        created-from-property (:logseq.property/created-from-property block)]
+    {:from-property-id (:db/id created-from-property)}))
+
+(defn class-add-property!
+  [conn class-id property-id]
+  (when-let [class (d/entity @conn class-id)]
+    (when (contains? (:block/type class) "class")
+      (let [[db-ident property options]
+            ;; strings come from user
+            (if (string? property-id)
+              (if-let [ent (ldb/get-case-page @conn property-id)]
+                [(:db/ident ent) ent {}]
+                ;; creates ident beforehand b/c needed in later transact and this avoids
+                ;; making this whole fn async for now
+                [(ensure-unique-db-ident
+                  @conn
+                  (db-property/create-user-property-ident-from-name property-id))
+                 nil
+                 {:property-name property-id}])
+              [property-id (d/entity @conn property-id) {}])
+            property-type (get-in property [:block/schema :type])
+            _ (upsert-property! conn
+                                db-ident
+                                (cond-> (:block/schema property)
+                                  (some? property-type)
+                                  (assoc :type property-type))
+                                options)]
+        (d/transact! conn
+                     [[:db/add (:db/id class) :class/schema.properties db-ident]]
+                     {:outliner-op :save-block})))))
+
+(defn class-remove-property!
+  [conn class-id property-id]
+  (when-let [class (d/entity @conn class-id)]
+    (when (contains? (:block/type class) "class")
+      (when-let [property (d/entity @conn property-id)]
+        (when-not (ldb/built-in-class-property? class property)
+          (d/transact! conn [[:db/retract (:db/id class) :class/schema.properties property-id]]
+                       {:outliner-op :save-block}))))))

+ 3 - 3
src/main/frontend/components/block.cljs

@@ -52,6 +52,7 @@
             [frontend.handler.export.common :as export-common-handler]
             [frontend.handler.property.util :as pu]
             [frontend.handler.db-based.property :as db-property-handler]
+            [logseq.outliner.property :as outliner-property]
             [frontend.mobile.util :as mobile-util]
             [frontend.mobile.intent :as mobile-intent]
             [frontend.modules.outliner.tree :as tree]
@@ -634,8 +635,7 @@
            :on-pointer-down
            (fn [e]
              (util/stop e)
-             (db-property-handler/delete-property-value! repo
-                                                         block
+             (db-property-handler/delete-property-value! (:db/id block)
                                                          :block/tags
                                                          (:db/id page-entity)))}
           (ui/icon "x" {:size 15})]))]))
@@ -2267,7 +2267,7 @@
 
 (rum/defc block-closed-values-properties
   [block]
-  (let [closed-values-properties (db-property-handler/get-block-other-position-properties (:db/id block))]
+  (let [closed-values-properties (outliner-property/get-block-other-position-properties (db/get-db) (:db/id block))]
     (when (seq closed-values-properties)
       [:div.closed-values-properties.flex.flex-row.items-center.gap-1.select-none.h-full
        (for [pid closed-values-properties]

+ 3 - 3
src/main/frontend/components/db_based/page.cljs

@@ -7,7 +7,7 @@
             [frontend.components.property.value :as pv]
             [frontend.config :as config]
             [frontend.db :as db]
-            [frontend.handler.db-based.property :as db-property-handler]
+            [logseq.outliner.property :as outliner-property]
             [frontend.ui :as ui]
             [frontend.state :as state]
             [rum.core :as rum]
@@ -26,7 +26,7 @@
         edit-input-id-prefix (str "edit-block-" (:block/uuid page))
         configure-opts {:selected? false
                         :page-configure? configure?}
-        has-viewable-properties? (db-property-handler/block-has-viewable-properties? page)
+        has-viewable-properties? (outliner-property/block-has-viewable-properties? page)
         has-class-properties? (seq (:class/schema.properties page))
         hide-properties? (:logseq.property/hide-properties? page)]
     (when (or configure?
@@ -94,7 +94,7 @@
 (rum/defc page-properties-react < rum/reactive
   [page* page-opts]
   (let [page (db/sub-block (:db/id page*))]
-    (when (or (db-property-handler/block-has-viewable-properties? page)
+    (when (or (outliner-property/block-has-viewable-properties? page)
               ;; Allow class and property pages to add new property
               (some #{"class" "property"} (:block/type page)))
       (page-properties page page-opts))))

+ 1 - 3
src/main/frontend/components/page.cljs

@@ -356,11 +356,9 @@
                 (icon-component/icon-picker icon
                                             {:on-chosen (fn [_e icon]
                                                           (db-property-handler/set-block-property!
-                                                           repo
                                                            (:db/id page)
                                                            (pu/get-pid :logseq.property/icon)
-                                                           icon
-                                                           {}))
+                                                           icon))
                                              :icon-props {:size 38}})
                 icon)])
            [:h1.page-title.flex-1.cursor-pointer.gap-1

+ 11 - 14
src/main/frontend/components/property.cljs

@@ -10,6 +10,7 @@
             [frontend.db.async :as db-async]
             [frontend.db-mixins :as db-mixins]
             [frontend.db.model :as model]
+            [logseq.outliner.property :as outliner-property]
             [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.notification :as notification]
             [frontend.handler.property :as property-handler]
@@ -110,11 +111,11 @@
             ;; Only ask for confirmation on class schema properties
               (js/confirm "Are you sure you want to delete this property?"))
       (let [repo (state/get-current-repo)
-            f (if (and class? class-schema?)
-                db-property-handler/class-remove-property!
-                property-handler/remove-block-property!)
+            [f id] (if (and class? class-schema?)
+                     [db-property-handler/class-remove-property! (:db/id block)]
+                     [property-handler/remove-block-property! (:block/uuid block)])
             property-id (:db/ident property)]
-        (f repo (:block/uuid block) property-id)))))
+        (f repo id property-id)))))
 
 (rum/defc schema-type <
   shortcut/disable-all-shortcuts
@@ -241,7 +242,6 @@
               (icon-component/icon-picker icon-value
                                           {:on-chosen (fn [_e icon]
                                                         (db-property-handler/upsert-property!
-                                                         (state/get-current-repo)
                                                          (:db/ident property)
                                                          (:block/schema property)
                                                          {:properties {:logseq.property/icon icon}}))})
@@ -249,7 +249,6 @@
               (when icon-value
                 [:a.fade-link.flex {:on-click (fn [_e]
                                                 (db-property-handler/remove-block-property!
-                                                 (state/get-current-repo)
                                                  (:db/ident property)
                                                  :logseq.property/icon))
                                     :title "Delete this icon"}
@@ -405,7 +404,7 @@
         (if (and (contains? (:block/type entity) "class") page-configure?)
           (pv/<add-property! entity property-uuid-or-name "" {:class-schema? class-schema? :exit-edit? page-configure?})
           (p/do!
-           (db-property-handler/upsert-property! repo nil {} {:property-name property-uuid-or-name})
+           (db-property-handler/upsert-property! nil {:type :default} {:property-name property-uuid-or-name})
            true))
         (do (notification/show! "This is an invalid property name. A property name cannot start with page reference characters '#' or '[['." :error)
             (pv/exit-edit-property))))))
@@ -518,7 +517,7 @@
            new-property?
            (property-input block *property-key *property-value opts)
 
-           (and (or (db-property-handler/block-has-viewable-properties? block)
+           (and (or (outliner-property/block-has-viewable-properties? block)
                     (:page-configure? opts))
                 (not config/publishing?)
                 (not (:in-block-container? opts)))
@@ -546,7 +545,6 @@
   (rum/local false ::hover?)
   [state block property {:keys [class-schema? block? collapsed? page-cp inline-text]}]
   (let [*hover? (::hover? state)
-        repo (state/get-current-repo)
         icon (:logseq.property/icon property)
         property-name (:block/original-name property)]
     [:div.flex.flex-row.items-center
@@ -574,7 +572,7 @@
        [:a.block-control
         {:on-click (fn [event]
                      (util/stop event)
-                     (db-property-handler/collapse-expand-property! repo block property (not collapsed?)))}
+                     (db-property-handler/collapse-expand-block-property! (:db/id block) (:db/id property) (not collapsed?)))}
         [:span {:class (cond
                          (or collapsed? @*hover?)
                          "control-show cursor-pointer"
@@ -588,8 +586,7 @@
                          {:on-chosen
                           (fn [_e icon]
                             (when icon
-                              (p/let [_ (db-property-handler/upsert-property! repo
-                                                                              (:db/ident property)
+                              (p/let [_ (db-property-handler/upsert-property! (:db/ident property)
                                                                               (:block/schema property)
                                                                               {:properties {:logseq.property/icon icon}})]
                                 (shui/popup-hide! id))))}))]
@@ -724,7 +721,7 @@
 (defn- async-load-classes!
   [block]
   (let [repo (state/get-current-repo)
-        classes (concat (:block/tags block) (db-property-handler/get-class-parents (:block/tags block)))]
+        classes (concat (:block/tags block) (outliner-property/get-class-parents (:block/tags block)))]
     (doseq [class classes]
       (db-async/<get-block repo (:db/id class) :children? false))
     classes))
@@ -762,7 +759,7 @@
                                                     (and (not (get-in ent [:block/schema :public?]))
                                                          (ldb/built-in? ent))))))
                                              properties))
-        {:keys [classes all-classes classes-properties]} (db-property-handler/get-block-classes-properties (:db/id block))
+        {:keys [classes all-classes classes-properties]} (outliner-property/get-block-classes-properties (db/get-db) (:db/id block))
         one-class? (= 1 (count classes))
         block-own-properties (->> (concat (when (seq (:block/alias block))
                                             [[:block/alias (:block/alias block)]])

+ 16 - 18
src/main/frontend/components/property/closed_value.cljs

@@ -21,13 +21,19 @@
             [logseq.db.frontend.order :as db-order]
             [logseq.outliner.core :as outliner-core]))
 
+(defn- re-init-commands!
+  "Update commands after task status and priority's closed values has been changed"
+  [property]
+  (when (contains? #{:logseq.task/status :logseq.task/priority} (:db/ident property))
+    (state/pub-event! [:init/commands])))
+
 (defn- <upsert-closed-value!
   "Create new closed value and returns its block UUID."
   [property item]
-  (p/let [{:keys [block-id tx-data]} (db-property-handler/<upsert-closed-value property item)]
+  (p/let [{:keys [block-id tx-data]} (db-property-handler/upsert-closed-value! property item)]
     (p/do!
      (when (seq tx-data) (db/transact! (state/get-current-repo) tx-data {:outliner-op :upsert-closed-value}))
-     (when (seq tx-data) (db-property-handler/re-init-commands! property))
+     (when (seq tx-data) (re-init-commands! property))
      block-id)))
 
 (rum/defc item-value
@@ -110,11 +116,10 @@
 
 (rum/defcs choice-with-close <
   (rum/local false ::hover?)
-  [state property item {:keys [toggle-fn delete-choice update-icon]} parent-opts]
+  [state item {:keys [toggle-fn delete-choice update-icon]} parent-opts]
   (let [*hover? (::hover? state)
         value (db-property/closed-value-name item)
         page? (:block/original-name item)
-        date? (= :date (:type (:block/schema property)))
         property-block? (db-property/property-created-block? item)]
     [:div.flex.flex-1.flex-row.items-center.gap-2.justify-between
      {:on-mouse-over #(reset! *hover? true)
@@ -128,14 +133,6 @@
         [:a {:on-click toggle-fn}
          value]
 
-        date?
-        [:div.flex.flex-row.items-center.gap-1
-         (property-value/date-picker item
-                                     {:on-change (fn [page]
-                                                   (db-property-handler/replace-closed-value property
-                                                                                             (:db/id page)
-                                                                                             (:db/id item)))})]
-
         (and page? (:page-cp parent-opts))
         ((:page-cp parent-opts) {:preview? false} item)
 
@@ -169,12 +166,13 @@
           opts {:toggle-fn #(shui/popup-show! % content-fn)}]
 
       (choice-with-close
-       property
        block
        (assoc opts
               :delete-choice
               (fn []
-                (db-property-handler/delete-closed-value! property block))
+                (p/do!
+                 (db-property-handler/delete-closed-value! (:db/id property) (:db/id block))
+                 (re-init-commands! property)))
               :update-icon
               (fn [icon]
                 (property-handler/set-block-property! (state/get-current-repo) (:block/uuid block) :logseq.property/icon icon)))
@@ -195,7 +193,7 @@
    (ui/button
     "Add choices"
     {:on-click (fn []
-                 (p/let [_ (db-property-handler/<add-existing-values-to-closed-values! property values)]
+                 (p/let [_ (db-property-handler/add-existing-values-to-closed-values! (:db/id property) values)]
                    (toggle-fn)))})])
 
 (rum/defc choices < rum/reactive
@@ -243,14 +241,14 @@
                    existing-values (seq (:property/closed-values property))
                    values (if (seq existing-values)
                             (let [existing-ids (set (map :db/id existing-values))]
-                              (remove (fn [[_ id]] (existing-ids id)) values))
+                              (remove (fn [id] (existing-ids id)) values))
                             values)]
              (shui/popup-show! (.-target e)
                                (fn [{:keys [id]}]
                                  (let [opts {:toggle-fn (fn [] (shui/popup-hide! id))}
                                        values' (->> (if (contains? db-property-type/ref-property-types (get-in property [:block/schema :type]))
-                                                      (map #(:block/uuid (db/entity (second %))) values)
-                                                      (map second values))
+                                                      (map #(:block/uuid (db/entity %)) values)
+                                                      values)
                                                     (remove string/blank?)
                                                     distinct)]
                                    (if (seq values')

+ 1 - 3
src/main/frontend/components/property/util.cljs

@@ -1,14 +1,12 @@
 (ns frontend.components.property.util
   "Property component utils"
-  (:require [frontend.state :as state]
-            [frontend.handler.db-based.property :as db-property-handler]))
+  (:require [frontend.handler.db-based.property :as db-property-handler]))
 
 (defn update-property!
   [property property-name property-schema]
   (when (or (not= (:block/original-name property) property-name)
             (not= (:block/schema property) property-schema))
     (db-property-handler/upsert-property!
-     (state/get-current-repo)
      (:db/ident property)
      property-schema
      {:property-name property-name})))

+ 34 - 35
src/main/frontend/components/property/value.cljs

@@ -10,6 +10,7 @@
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.property :as property-handler]
+            [logseq.outliner.property :as outliner-property]
             [frontend.handler.db-based.property :as db-property-handler]
             [frontend.state :as state]
             [frontend.ui :as ui]
@@ -19,7 +20,6 @@
             [rum.core :as rum]
             [frontend.handler.route :as route-handler]
             [promesa.core :as p]
-            [goog.dom :as gdom]
             [frontend.db.async :as db-async]
             [logseq.common.util.macro :as macro-util]
             [logseq.db :as ldb]
@@ -40,15 +40,12 @@
                                  {:disabled? config/publishing?
                                   :on-chosen (fn [_e icon]
                                                (db-property-handler/set-block-property!
-                                                (state/get-current-repo)
                                                 (:db/id block)
                                                 :logseq.property/icon
-                                                icon
-                                                {}))})
+                                                icon))})
      (when (and icon-value (not config/publishing?))
        [:a.fade-link.flex {:on-click (fn [_e]
                                        (db-property-handler/remove-block-property!
-                                        (state/get-current-repo)
                                         (:db/id block)
                                         :logseq.property/icon))
                            :title "Delete this icon"}
@@ -71,12 +68,15 @@
 (defn <create-new-block!
   [block property value & {:keys [edit-block?]
                            :or {edit-block? true}}]
-  (p/let [{:keys [block-id result]} (db-property-handler/create-property-text-block!
-                                     block property value editor-handler/wrap-parse-block {})]
+  (p/let [new-block-id (db/new-block-id)
+          _ (db-property-handler/create-property-text-block!
+             (:db/id block)
+             (:db/id property)
+             value
+             {:new-block-id new-block-id})]
     (p/do!
-     result
      (exit-edit-property)
-     (let [block (db/entity [:block/uuid block-id])]
+     (let [block (db/entity [:block/uuid new-block-id])]
        (when edit-block?
          (editor-handler/edit-block! block :max {:container-id :unknown-container}))
        block))))
@@ -89,28 +89,29 @@
                                        :or {exit-edit? true}}]
 
    (let [repo (state/get-current-repo)
-         class? (contains? (:block/type block) "class")]
+         class? (contains? (:block/type block) "class")
+         property (db/entity property-key)]
      (p/do!
       (when property-key
         (if (and class? class-schema?)
-          (db-property-handler/class-add-property! repo (:block/uuid block) property-key)
+          (db-property-handler/class-add-property! (:db/id block) property-key)
           (let [[property-id property-value']
                 (if (string? property-key)
                   (if-let [ent (ldb/get-case-page (db/get-db repo) property-key)]
                     [(:db/ident ent) property-value]
                     ;; This is a new property. Create a new property id to use of set-block-property!
-                    [(db-property-handler/ensure-unique-db-ident
+                    [(outliner-property/ensure-unique-db-ident
                       (db/get-db (state/get-current-repo))
                       (db-property/create-user-property-ident-from-name property-key))
-                     :logseq.property/empty-placeholder])
+                     (if (= :checkbox (get-in property [:block/schema :type]))
+                       false
+                       :logseq.property/empty-placeholder)])
                   [property-key property-value])]
-            (p/let [property (db/entity property-key)
-                    value (if (and (db-property-type/ref-property-types (get-in property [:block/schema :type]))
-                                   (not (int? property-value')))
-                            (p/let [result (<create-new-block! block (db/entity property-id) property-value' {:edit-block? false})]
-                              (:db/id result))
-                            property-value')]
-              (property-handler/set-block-property! repo (:block/uuid block) property-id value)))))
+            (p/let [property (db/entity property-key)]
+              (if (and (db-property-type/ref-property-types (get-in property [:block/schema :type]))
+                       (not (int? property-value')))
+                (<create-new-block! block (db/entity property-id) property-value' {:edit-block? false})
+                (property-handler/set-block-property! repo (:block/uuid block) property-id property-value'))))))
       (when exit-edit?
         (shui/popup-hide!)
         (exit-edit-property))))))
@@ -377,11 +378,17 @@
 (defn <create-new-block-from-template!
   "`template`: tag block"
   [block property template]
-  (let [repo (state/get-current-repo)
-        {:keys [page blocks]} (db-property-handler/property-create-new-block-from-template block property template)]
-    (p/let [_ (db/transact! repo (if page (cons page blocks) blocks) {:outliner-op :insert-blocks})
-            _ (<add-property! block (:db/ident property) (:block/uuid (last blocks)))]
-      (last blocks))))
+  (p/let [new-block-id (db/new-block-id)
+          _ (db-property-handler/create-property-text-block!
+             (:db/id block)
+             (:db/id property)
+             ""
+             {:new-block-id new-block-id
+              :template-id (:db/id template)})
+          new-block (db/entity [:block/uuid new-block-id])]
+    (shui/popup-hide!)
+    (exit-edit-property)
+    new-block))
 
 (rum/defcs select < rum/reactive
   {:init (fn [state]
@@ -411,7 +418,7 @@
                                         value)
                                :value (:db/id block)})) (:property/closed-values property))
                     (->> values
-                         (mapcat (fn [[_id value]]
+                         (mapcat (fn [value]
                                    (if (coll? value)
                                      (map (fn [v] {:value v}) value)
                                      [{:value value}])))
@@ -686,14 +693,6 @@
         (editor-box editor-args editor-id config)])
      nil)])
 
-(defn- set-editing!
-  [block property editor-id dom-id v opts]
-  (let [v (str v)
-        cursor-range (if dom-id
-                       (some-> (gdom/getElement dom-id) util/caret-range)
-                       "")]
-    (state/set-editing! editor-id v property cursor-range (assoc opts :property-block block))))
-
 (defn- property-value-inner
   [block property value {:keys [inline-text block-cp page-cp
                                 editor-id dom-id row?
@@ -829,7 +828,7 @@
          [editing?])
 
         (if (and dropdown? (not editing?))
-          (let [toggle-fn #(shui/popup-hide!)
+          (let [toggle-fn shui/popup-hide!
                 content-fn (fn [{:keys [_id content-props]}]
                              (select-cp {:content-props content-props}))]
             [:div.multi-values.jtrigger

+ 17 - 1
src/main/frontend/db/async.cljs

@@ -80,13 +80,29 @@
 (defn <get-block-property-values
   [graph property-id]
   (<q graph {:transact-db? false}
-      '[:find ?b ?v
+      '[:find [?v ...]
         :in $ ?property-id
         :where
         [?b ?property-id ?v]
         [(not= ?v :logseq.property/empty-placeholder)]]
       property-id))
 
+(comment
+  (defn <get-block-property-value-entity
+    [graph property-id value]
+    (p/let [result (<q graph {}
+                       '[:find [(pull ?vid [*]) ...]
+                         :in $ ?property-id ?value
+                         :where
+                         [?b ?property-id ?vid]
+                         [(not= ?vid :logseq.property/empty-placeholder)]
+                         (or
+                          [?vid :block/content ?value]
+                          [?vid :block/original-name ?value])]
+                       property-id
+                       value)]
+      (db/entity (:db/id (first result))))))
+
 ;; TODO: batch queries for better performance and UX
 (defn <get-block
   [graph name-or-uuid & {:keys [children? nested-children?]

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

@@ -368,7 +368,7 @@
                         (concat tx-data
                                 (db-fix/fix-cardinality-many->one @conn (:property-id tx-meta)))
                         tx-data)
-             tx-data' (if (contains? #{:new-property :insert-blocks} (:outliner-op tx-meta))
+             tx-data' (if (contains? #{:insert-blocks} (:outliner-op tx-meta))
                         (map (fn [m]
                                (if (and (map? m) (nil? (:block/order m)))
                                  (assoc m :block/order (db-order/gen-key nil))

+ 75 - 590
src/main/frontend/handler/db_based/property.cljs

@@ -1,610 +1,95 @@
 (ns frontend.handler.db-based.property
-  "Properties handler for db graphs"
-  (:require [clojure.string :as string]
-            [frontend.db :as db]
-            [frontend.format.block :as block]
-            [frontend.handler.notification :as notification]
-            [frontend.handler.db-based.property.util :as db-pu]
-            [logseq.outliner.core :as outliner-core]
-            [frontend.util :as util]
-            [frontend.state :as state]
-            [logseq.common.util :as common-util]
-            [logseq.db.sqlite.util :as sqlite-util]
-            [logseq.db.frontend.property.type :as db-property-type]
-            [logseq.db.frontend.property.build :as db-property-build]
-            [malli.util :as mu]
-            [malli.error :as me]
-            [logseq.common.util.page-ref :as page-ref]
-            [datascript.core :as d]
-            [datascript.impl.entity :as e]
+  "db based property handler"
+  (:require [frontend.modules.outliner.ui :as ui-outliner-tx]
+            [frontend.modules.outliner.op :as outliner-op]
             [logseq.db.frontend.property :as db-property]
-            [frontend.handler.property.util :as pu]
-            [promesa.core :as p]
-            [logseq.db :as ldb]
-            [logseq.db.frontend.malli-schema :as db-malli-schema]
-            [logseq.db.frontend.order :as db-order]))
-
-;; schema -> type, cardinality, object's class
-;;           min, max -> string length, number range, cardinality size limit
-
-(defn- build-property-value-tx-data
-  ([db block property-id value]
-   (build-property-value-tx-data db block property-id value (= property-id :logseq.task/status)))
-  ([db block property-id value status?]
-   (when (some? value)
-     (let [block (assoc (outliner-core/block-with-updated-at {:db/id (:db/id block)})
-                        property-id value)
-           block-tx-data (cond-> block
-                          status?
-                           (assoc :block/tags :logseq.class/task))]
-       [block-tx-data]))))
-
-(defn- get-property-value-schema
-  "Gets a malli schema to validate the property value for the given property type and builds
-   it with additional args like datascript db"
-  [property-type property & {:keys [new-closed-value?]
-                             :or {new-closed-value? false}}]
-  (let [property-val-schema (or (get db-property-type/built-in-validation-schemas property-type)
-                                (throw (ex-info (str "No validation for property type " (pr-str property-type)) {})))
-        [schema-opts schema-fn] (if (vector? property-val-schema)
-                                  (rest property-val-schema)
-                                  [{} property-val-schema])]
-    [:fn
-     schema-opts
-     (fn property-value-schema [property-val]
-       (db-malli-schema/validate-property-value (db/get-db) schema-fn [property property-val] {:new-closed-value? new-closed-value?}))]))
-
-(defn- fail-parse-long
-  [v-str]
-  (let [result (parse-long v-str)]
-    (or result
-        (throw (js/Error. (str "Can't convert \"" v-str "\" to a number"))))))
-
-(defn- fail-parse-double
-  [v-str]
-  (let [result (parse-double v-str)]
-    (or result
-        (throw (js/Error. (str "Can't convert \"" v-str "\" to a number"))))))
-
-(defn- infer-schema-from-input-string
-  [v-str]
-  (try
-    (cond
-      (fail-parse-long v-str) :number
-      (fail-parse-double v-str) :number
-      (common-util/url? v-str) :url
-      (contains? #{"true" "false"} (string/lower-case v-str)) :checkbox
-      :else :default)
-    (catch :default _e
-      :default)))
-
-(defn convert-property-input-string
-  [schema-type v-str]
-  (if (and (= :number schema-type) (string? v-str))
-    (fail-parse-double v-str)
-    v-str))
-
-(defn- update-datascript-schema
-  [property {:keys [cardinality]}]
-  (let [ident (:db/ident property)
-        cardinality (if (= cardinality :many) :db.cardinality/many :db.cardinality/one)]
-    {:db/ident ident
-     :db/valueType :db.type/ref
-     :db/cardinality cardinality}))
-
-(defn ensure-unique-db-ident
-  "Ensures the given db-ident is unique. If a db-ident conflicts, it is made
-  unique by adding a suffix with a unique number e.g. :db-ident-1 :db-ident-2"
-  [db db-ident]
-  (if (d/entity db db-ident)
-    (let [existing-idents
-          (d/q '[:find [?ident ...]
-                 :in $ ?ident-name
-                 :where
-                 [?b :db/ident ?ident]
-                 [(str ?ident) ?str-ident]
-                 [(clojure.string/starts-with? ?str-ident ?ident-name)]]
-               db
-               (str db-ident "-"))
-          new-ident (if-let [max-num (->> existing-idents
-                                          (keep #(parse-long (string/replace-first (str %) (str db-ident "-") "")))
-                                          (apply max))]
-                      (keyword (namespace db-ident) (str (name db-ident) "-" (inc max-num)))
-                      (keyword (namespace db-ident) (str (name db-ident) "-1")))]
-      new-ident)
-    db-ident))
+            [frontend.db :as db]
+            #_:clj-kondo/ignore
+            [frontend.state :as state]))
 
 (defn upsert-property!
-  "Updates property if property-id is given. Otherwise creates a property
-   with the given property-id or :property-name option. When a property is created
-   it is ensured to have a unique :db/ident"
-  [repo property-id schema {:keys [property-name properties]}]
-  (let [db-ident (or property-id (db-property/create-user-property-ident-from-name property-name))
-        db (db/get-db repo)]
-    (assert (qualified-keyword? db-ident))
-    (if-let [property (and (qualified-keyword? property-id) (db/entity db-ident))]
-      (let [changed-property-attrs
-            ;; Only update property if something has changed as we are updating a timestamp
-            (cond-> {}
-              (not= schema (:block/schema property))
-              (assoc :block/schema schema)
-              (and (some? property-name) (not= property-name (:block/original-name property)))
-              (assoc :block/original-name property-name))
-            property-tx-data
-            (cond-> []
-              (seq changed-property-attrs)
-              (conj (outliner-core/block-with-updated-at
-                     (merge {:db/ident db-ident}
-                            (common-util/dissoc-in changed-property-attrs [:block/schema :cardinality]))))
-              (or (not= (:type schema) (get-in property [:block/schema :type]))
-                  (and (:cardinality schema) (not= (:cardinality schema) (keyword (name (:db/cardinality property)))))
-                  (and (= :default (:type schema)) (not= :db.type/ref (:db/valueType property)))
-                  (seq (:property/closed-values property)))
-              (conj (update-datascript-schema property schema)))
-            tx-data (concat property-tx-data
-                            (when (seq properties)
-                              (mapcat
-                               (fn [[property-id v]]
-                                 (build-property-value-tx-data db property property-id v)) properties)))
-            many->one? (and (db-property/many? property) (= :one (:cardinality schema)))]
-        (when (seq tx-data)
-          (db/transact! repo tx-data {:outliner-op :update-property
-                                      :property-id (:db/id property)
-                                      :many->one? many->one?})))
-      (let [k-name (or (and property-name (name property-name))
-                       (name property-id))
-            db-ident' (ensure-unique-db-ident (db/get-db repo) db-ident)]
-        (assert (some? k-name)
-                (prn "property-id: " property-id ", property-name: " property-name))
-        (db/transact! repo
-                      [(sqlite-util/build-new-property db-ident' schema {:original-name k-name
-                                                                         :from-ui-thread? true})]
-                      {:outliner-op :new-property})))))
-
-(defn validate-property-value
-  [schema value]
-  (me/humanize (mu/explain-data schema value)))
-
-(defn- resolve-tag
-  "Change `v` to a tag's db id if v is a string tag, e.g. `#book`"
-  [v]
-  (when (and (string? v)
-             (util/tag? (string/trim v)))
-    (let [tag-without-hash (common-util/safe-subs (string/trim v) 1)
-          tag (or (page-ref/get-page-name tag-without-hash) tag-without-hash)]
-      (when-not (string/blank? tag)
-        (let [e (db/get-case-page tag)
-              e' (if e
-                   (do
-                     (when-not (contains? (:block/type e) "tag")
-                       (db/transact! [{:db/id (:db/id e)
-                                       :block/type (set (conj (:block/type e) "class"))}]))
-                     e)
-                   (let [m (assoc (block/page-name->map tag true)
-                                  :block/type #{"class"})]
-                     (db/transact! [m])
-                     m))]
-          (:db/id e'))))))
-
-(defn- ->eid
-  [id]
-  (if (uuid? id) [:block/uuid id] id))
+  [property-id schema property-opts]
+  (ui-outliner-tx/transact!
+   {:outliner-op :upsert-property}
+    (outliner-op/upsert-property! property-id schema property-opts)))
 
 (defn set-block-property!
-  "Updates a block property's value for the an existing property-id. If possibly
-  creating a new property, use upsert-property!"
-  [repo block-eid property-id v {:keys [property-name property-type]}]
-  (let [block-eid (->eid block-eid)
-        _ (assert (qualified-keyword? property-id) "property-id should be a keyword")
-        block (db/entity repo block-eid)
-        property (db/entity property-id)
-        k-name (:block/original-name property)
-        property-schema (:block/schema property)
-        {:keys [type]} property-schema
-        v' (or (resolve-tag v) v)
-        db-attribute? (contains? db-property/db-attribute-properties property-id)
-        db (db/get-db repo)]
-    (cond
-      db-attribute?
-      (db/transact! repo [{:db/id (:db/id block) property-id v'}]
-                    {:outliner-op :save-block})
-
-      :else
-      (let [v'' (if property v' (or v' ""))]
-        (when (some? v'')
-          (let [infer-schema (when-not type (infer-schema-from-input-string v''))
-                property-type' (or type property-type infer-schema :default)
-                schema (get-property-value-schema property-type' (or property
-                                                                     {:block/schema {:type property-type'}}))
-                existing-value (when-let [id (:db/ident property)]
-                                 (get block id))
-                new-value* (if (= v'' :logseq.property/empty-placeholder)
-                             v''
-                             (try
-                               (convert-property-input-string property-type' v'')
-                               (catch :default e
-                                 (js/console.error e)
-                                 (notification/show! (str e) :error false)
-                                 nil)))]
-            (when-not (= existing-value new-value*)
-              (if-let [msg (validate-property-value schema
-                                                    ;; normalize :many values for components that only provide single value
-                                                    (if (and (db-property/many? property) (not (coll? new-value*)))
-                                                      #{new-value*}
-                                                      new-value*))]
-                (let [msg' (str "\"" k-name "\"" " " (if (coll? msg) (first msg) msg))]
-                  (notification/show! msg' :warning))
-                (let [_ (upsert-property! repo property-id (assoc property-schema :type property-type') {:property-name property-name})
-                      status? (= :logseq.task/status (:db/ident property))
-                      ;; don't modify maps
-                      new-value (if (or (sequential? new-value*) (set? new-value*))
-                                  (if (= :coll property-type')
-                                    (vec (remove string/blank? new-value*))
-                                    (set (remove string/blank? new-value*)))
-                                  new-value*)
-                      tx-data (build-property-value-tx-data db block property-id new-value status?)]
-                  (db/transact! repo tx-data {:outliner-op :save-block}))))))))))
-
-(defn class-add-property!
-  [repo class-uuid property-id]
-  (when-let [class (db/entity repo [:block/uuid class-uuid])]
-    (when (contains? (:block/type class) "class")
-      (let [[db-ident property options]
-            ;; strings come from user
-            (if (string? property-id)
-              (if-let [ent (ldb/get-case-page (db/get-db repo) property-id)]
-                [(:db/ident ent) ent {}]
-                ;; creates ident beforehand b/c needed in later transact and this avoids
-                ;; making this whole fn async for now
-                [(ensure-unique-db-ident
-                  (db/get-db (state/get-current-repo))
-                  (db-property/create-user-property-ident-from-name property-id))
-                 nil
-                 {:property-name property-id}])
-              [property-id (db/entity property-id) {}])
-            property-type (get-in property [:block/schema :type])
-            _ (upsert-property! repo
-                                db-ident
-                                (cond-> (:block/schema property)
-                                  (some? property-type)
-                                  (assoc :type property-type))
-                                options)]
-        (db/transact! repo
-                      [[:db/add (:db/id class) :class/schema.properties db-ident]]
-                      {:outliner-op :save-block})))))
-
-(defn class-remove-property!
-  [repo class-uuid property-id]
-  (when-let [class (db/entity repo [:block/uuid class-uuid])]
-    (when (contains? (:block/type class) "class")
-      (when-let [property (db/entity repo property-id)]
-        (when-not (ldb/built-in-class-property? class property)
-          (db/transact! repo [[:db/retract (:db/id class) :class/schema.properties property-id]]
-                        {:outliner-op :save-block}))))))
-
-(defn batch-set-property!
-  "Notice that this works only for properties with cardinality equals to `one`."
-  [repo block-ids property-id v]
-  (assert property-id "property-id is nil")
-  (let [block-eids (map ->eid block-ids)
-        property (db/entity property-id)]
-    (when property
-      (let [type (:type (:block/schema property))
-            infer-schema (when-not type (infer-schema-from-input-string v))
-            property-type (or type infer-schema :default)
-            many? (db-property/many? property)
-            status? (= :logseq.task/status (:db/ident property))
-            db (db/get-db repo)
-            txs (->>
-                 (mapcat
-                  (fn [eid]
-                    (when-let [block (db/entity eid)]
-                      (when (and (some? v) (not many?))
-                        (when-let [v* (try
-                                        (convert-property-input-string property-type v)
-                                        (catch :default e
-                                          (notification/show! (str e) :error false)
-                                          nil))]
-                          (build-property-value-tx-data db block property-id v* status?)))))
-                  block-eids)
-                 (remove nil?))]
-        (when (seq txs)
-          (db/transact! repo txs {:outliner-op :save-block}))))))
-
-(defn batch-remove-property!
-  [repo block-ids property-id]
-  (let [block-eids (map ->eid block-ids)]
-    (when-let [property (db/entity property-id)]
-      (let [txs (mapcat
-                 (fn [eid]
-                   (when-let [block (db/entity eid)]
-                     (let [value (get block property-id)
-                           block-value? (= :default (get-in property [:block/schema :type] :default))
-                           property-block (when block-value? (db/entity (:db/id value)))
-                           retract-blocks-tx (when (and property-block
-                                                        (some? (get property-block :logseq.property/created-from-property)))
-                                               (let [txs-state (atom [])]
-                                                 (outliner-core/delete-block repo
-                                                                             (db/get-db false)
-                                                                             txs-state
-                                                                             property-block
-                                                                             {:children? true})
-                                                 @txs-state))]
-                       (concat
-                        [[:db/retract eid (:db/ident property)]]
-                        retract-blocks-tx))))
-                 block-eids)]
-        (when (seq txs)
-          (db/transact! repo txs {:outliner-op :save-block}))))))
+  [block-id property-id value]
+  (ui-outliner-tx/transact!
+   {:outliner-op :set-block-property}
+   (outliner-op/set-block-property! block-id property-id value)))
 
 (defn remove-block-property!
-  [repo eid property-id]
-  (let [eid (->eid eid)]
-    (if (contains? db-property/db-attribute-properties property-id)
-     (when-let [block (db/entity eid)]
-       (db/transact! repo
-                     [[:db/retract (:db/id block) property-id]]
-                     {:outliner-op :save-block}))
-     (batch-remove-property! repo [eid] property-id))))
+  [block-id property-id]
+  (ui-outliner-tx/transact!
+   {:outliner-op :remove-block-property}
+    (outliner-op/remove-block-property! block-id property-id)))
 
 (defn delete-property-value!
-  "Delete value if a property has multiple values"
-  [repo block property-id property-value]
-  (when block
-    (when (not= property-id (:db/ident block))
-      (when-let [property (db/entity property-id)]
-        (if (db-property/many? property)
-          (db/transact! repo
-                        [[:db/retract (:db/id block) property-id property-value]]
-                        {:outliner-op :save-block})
-          (if (= :default (get-in property [:block/schema :type]))
-            (set-block-property! repo (:db/id block)
-                                 (:db/ident property)
-                                 ""
-                                 {})
-            (remove-block-property! repo (:db/id block) property-id)))))))
-
-(defn collapse-expand-property!
-  "Notice this works only if the value itself if a block (property type should be either :default or :template)"
-  [repo block property collapse?]
-  (let [f (if collapse? :db/add :db/retract)]
-    (db/transact! repo
-                  [[f (:db/id block) :block/collapsed-properties (:db/id property)]]
-                  {:outliner-op :save-block})))
-
-(defn get-class-parents
-  [tags]
-  (let [tags' (filter (fn [tag] (contains? (:block/type tag) "class")) tags)
-        *classes (atom #{})]
-    (doseq [tag tags']
-      (when-let [parent (:class/parent tag)]
-        (loop [current-parent parent]
-          (when (and
-                 current-parent
-                 (contains? (:block/type parent) "class")
-                 (not (contains? @*classes (:db/id parent))))
-            (swap! *classes conj current-parent)
-            (recur (:class/parent current-parent))))))
-    @*classes))
-
-(defn get-block-classes-properties
-  [eid]
-  (let [block (db/entity eid)
-        classes (->> (:block/tags block)
-                     (sort-by :block/name)
-                     (filter (fn [tag] (contains? (:block/type tag) "class"))))
-        class-parents (get-class-parents classes)
-        all-classes (->> (concat classes class-parents)
-                         (filter (fn [class]
-                                   (seq (:class/schema.properties class)))))
-        all-properties (-> (mapcat (fn [class]
-                                     (map :db/ident (:class/schema.properties class))) all-classes)
-                           distinct)]
-    {:classes classes
-     :all-classes all-classes           ; block own classes + parent classes
-     :classes-properties all-properties}))
-
-(defn- closed-value-other-position?
-  [property-id block-properties]
-  (and
-   (some? (get block-properties property-id))
-   (let [schema (:block/schema (db/entity property-id))]
-     (= (:position schema) "block-beginning"))))
-
-(defn get-block-other-position-properties
-  [eid]
-  (let [block (db/entity eid)
-        own-properties (keys (:block/properties block))]
-    (->> (:classes-properties (get-block-classes-properties eid))
-         (concat own-properties)
-         (filter (fn [id] (closed-value-other-position? id (:block/properties block))))
-         (distinct))))
-
-(defn block-has-viewable-properties?
-  [block-entity]
-  (let [properties (:block/properties block-entity)]
-    (or
-     (seq (:block/alias block-entity))
-     (and (seq properties)
-          (not= properties [:logseq.property/icon])))))
+  [block-id property-id property-value]
+  (ui-outliner-tx/transact!
+   {:outliner-op :delete-property-value}
+    (outliner-op/delete-property-value! block-id property-id property-value)))
 
 (defn create-property-text-block!
-  [block property value parse-block {:keys [class-schema?]}]
-  (assert (e/entity? property))
-  (let [repo (state/get-current-repo)
-        new-value-block (db-property-build/build-property-value-block block property value parse-block)
-        class? (contains? (:block/type block) "class")
-        property-id (:db/ident property)]
-    (p/let [_ (db/transact! repo [new-value-block] {:outliner-op :insert-blocks})]
-      (let [result (when property-id
-                     (if (and class? class-schema?)
-                       (class-add-property! repo (:db/id block) property-id)
-                       (when-let [block-id (:db/id (db/entity [:block/uuid (:block/uuid new-value-block)]))]
-                         (set-block-property! repo (:db/id block) property-id block-id {}))))]
-        {:block-id (:block/uuid new-value-block)
-         :result result}))))
-
-(defn property-create-new-block-from-template
-  [_block property template]
-  (let [page-name (str "$$$" (:block/uuid property))
-        page-entity (db/get-case-page page-name)
-        page (or page-entity
-                 (-> (block/page-name->map page-name true)
-                     (assoc :block/type #{"hidden"}
-                            :block/format :markdown
-                            :logseq.property/source-page (:db/id property))))
-        page-tx (when-not page-entity page)
-        page-id [:block/uuid (:block/uuid page)]
-        block-id (db/new-block-id)
-        new-block (-> {:block/uuid block-id
-                       :block/format :markdown
-                       :block/content ""
-                       :block/tags #{(:db/id template)}
-                       :block/page page-id
-                       :block/parent page-id
-                       :logseq.property/created-from-property (:db/id property)
-                       :logseq.property/created-from-template [:block/uuid (:block/uuid template)]}
-                      sqlite-util/block-with-timestamps)]
-    {:page page-tx
-     :blocks [new-block]}))
-
-(defn re-init-commands!
-  "Update commands after task status and priority's closed values has been changed"
-  [property]
-  (when (contains? #{:logseq.task/status :logseq.task/priority} (:db/ident property))
-    (state/pub-event! [:init/commands])))
-
-(defn replace-closed-value
-  [property new-id old-id]
-  (assert (and (uuid? new-id) (uuid? old-id)))
-  (db/transact! (state/get-current-repo)
-                [[:db/retract [:block/uuid old-id] :block/closed-value-property (:db/id property)]
-                 [:db/add [:block/uuid new-id] :block/closed-value-property (:db/id property)]]
-                {:outliner-op :save-block}))
-
-(defn <upsert-closed-value
-  "id should be a block UUID or nil"
-  [property {:keys [id value icon description]
-             :or {description ""}}]
-  (assert (or (nil? id) (uuid? id)))
-  (let [property-type (get-in property [:block/schema :type] :default)]
-    (when (contains? db-property-type/closed-value-property-types property-type)
-      (p/let [property (db/entity (:db/id property))
-              value (if (string? value) (string/trim value) value)
-              property-schema (:block/schema property)
-              closed-values (:property/closed-values property)
-              default-closed-values? (and (= :default property-type) (seq closed-values))
-              value (if (and default-closed-values? (string? value) (not (string/blank? value)))
-                      (p/let [result (create-property-text-block! nil property value nil {})]
-                        (:db/id (db/entity [:block/uuid (:block-id result)])))
-                      value)
-              resolved-value (try
-                               (convert-property-input-string (:type property-schema) value)
-                               (catch :default e
-                                 (js/console.error e)
-                                 (notification/show! (str e) :error false)
-                                 nil))
-              block (when id (db/entity [:block/uuid id]))
-              validate-message (validate-property-value
-                                (get-property-value-schema property-type property {:new-closed-value? true})
-                                resolved-value)]
-        (cond
-          (some (fn [b]
-                  (and (= (str resolved-value) (str (or (db-pu/property-value-when-closed b)
-                                                        (:block/uuid b))))
-                       (not= id (:block/uuid b)))) closed-values)
-          (do
-            (notification/show! "Choice already exists" :warning)
-            :value-exists)
+  [block-id property-id value opts]
+  (ui-outliner-tx/transact!
+   {:outliner-op :create-property-text-block}
+    (outliner-op/create-property-text-block! block-id property-id value opts)))
 
-          validate-message
-          (do
-            (notification/show! validate-message :warning)
-            :value-invalid)
+(defn collapse-expand-block-property!
+  [block-id property-id collapse?]
+  (ui-outliner-tx/transact!
+   {:outliner-op :collapse-expand-block-property}
+   (outliner-op/collapse-expand-block-property! block-id property-id collapse?)))
 
-          (nil? resolved-value)
-          nil
-
-          :else
-          (let [block-id (or id (db/new-block-id))
-                icon (when-not (and (string? icon) (string/blank? icon)) icon)
-                description (string/trim description)
-                description (when-not (string/blank? description) description)
-                tx-data (if block
-                          [(let [schema (:block/schema block)]
-                             (cond->
-                              (outliner-core/block-with-updated-at
-                               {:block/uuid id
-                                :block/content resolved-value
-                                :block/closed-value-property (:db/id property)
-                                :block/schema (if description
-                                                (assoc schema :description description)
-                                                (dissoc schema :description))})
-                               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
-                                                                                                property {:icon icon
-                                                                                                          :description description})
-                                              (assoc :block/order (db-order/gen-key max-order nil)))]
-                            [new-block
-                             (outliner-core/block-with-updated-at
-                              {:db/id (:db/id property)})]))]
-            {:block-id block-id
-             :tx-data tx-data}))))))
-
-(defn <add-existing-values-to-closed-values!
-  "Adds existing values as closed values and returns their new block uuids"
-  [property values]
-  (assert (e/entity? property))
-  (when (seq values)
-    (let [values' (remove string/blank? values)]
-      (assert (every? uuid? values') "existing values should all be UUIDs")
-      (p/let [values (keep #(db/entity [:block/uuid %]) values')]
-        (when (seq values)
-          (let [value-property-tx (map (fn [id]
-                                         {:db/id id
-                                          :block/type "closed value"
-                                          :block/closed-value-property (:db/id property)})
-                                       (map :db/id values))
-                property-tx (outliner-core/block-with-updated-at {:db/id (:db/id property)})]
-            (db/transact! (state/get-current-repo) (cons property-tx value-property-tx)
-                          {:outliner-op :save-blocks})))))))
-
-(defn delete-closed-value!
-  "Returns true when deleted or if not deleted displays warning and returns false"
-  [property value-block]
-  (cond
-    (ldb/built-in? value-block)
-    (do (notification/show! "The choice can't be deleted because it's built-in." :warning)
-        false)
+(defn batch-set-property!
+  [block-id property-id value]
+  (ui-outliner-tx/transact!
+   {:outliner-op :batch-set-property}
+    (outliner-op/batch-set-property! block-id property-id value)))
 
-    (seq (:block/_refs value-block))
-    (do (notification/show! "The choice can't be deleted because it's still used." :warning)
-        false)
+(defn batch-remove-property!
+  [block-id property-id]
+  (ui-outliner-tx/transact!
+   {:outliner-op :batch-remove-property}
+    (outliner-op/batch-remove-property! block-id property-id)))
 
-    :else
-    (let [property (db/entity (:db/id property))
-          tx-data [[:db/retractEntity (:db/id value-block)]
-                   (outliner-core/block-with-updated-at
-                    {:db/id (:db/id property)})]]
-      (p/do!
-       (db/transact! tx-data)
-       (re-init-commands! property)
-       true))))
+(defn class-add-property!
+  [class-id property-id]
+  (ui-outliner-tx/transact!
+   {:outliner-op :class-add-property}
+    (outliner-op/class-add-property! class-id property-id)))
 
-(defn get-property-block-created-block
-  "Get the root block and property that created this property block."
-  [eid]
-  (let [block (db/entity eid)
-        created-from-property (:logseq.property/created-from-property block)]
-    {:from-property-id (:db/id created-from-property)}))
+(defn class-remove-property!
+  [class-id property-id]
+  (ui-outliner-tx/transact!
+   {:outliner-op :class-remove-property}
+    (outliner-op/class-remove-property! class-id property-id)))
 
 (defn batch-set-property-closed-value!
   [block-ids db-ident closed-value]
-  (if-let [closed-value-entity (pu/get-closed-value-entity-by-name db-ident closed-value)]
-    (batch-set-property! (state/get-current-repo)
-                         block-ids
-                         db-ident
-                         (:db/id closed-value-entity))
-    (js/console.error (str "No entity found for closed value " (pr-str closed-value)))))
+  (let [db (db/get-db)]
+    (if-let [closed-value-entity (db-property/get-closed-value-entity-by-name db db-ident closed-value)]
+      (batch-set-property! block-ids
+                           db-ident
+                           (:db/id closed-value-entity))
+      (js/console.error (str "No entity found for closed value " (pr-str closed-value))))))
+
+(defn upsert-closed-value!
+  [property-id closed-value-config]
+  (ui-outliner-tx/transact!
+   {:outliner-op :upsert-closed-value}
+   (outliner-op/upsert-closed-value! property-id closed-value-config)))
+
+(defn delete-closed-value!
+  [property-id value]
+  (ui-outliner-tx/transact!
+   {:outliner-op :delete-closed-value}
+    (outliner-op/delete-closed-value! property-id value)))
+
+(defn add-existing-values-to-closed-values!
+  [property-id values]
+  (ui-outliner-tx/transact!
+   {:outliner-op :add-existing-values-to-closed-values}
+    (outliner-op/add-existing-values-to-closed-values! property-id values)))

+ 0 - 7
src/main/frontend/handler/db_based/property/util.cljs

@@ -15,7 +15,6 @@
   (every? (fn [id]
             (:hide? (:block/schema (db/entity id)))) properties))
 
-;; FIXME: property no long has `:block/name` attribute
 (defn readable-properties
   "Given a DB graph's properties, returns a readable properties map with keys as
   property names and property values dereferenced where possible. A property's
@@ -34,9 +33,3 @@
                   (set (map readable-property-val v))
                   (readable-property-val v))])))
        (into {})))
-
-(defn property-value-when-closed
-  "Returns property value if the given entity is type 'closed value' or nil"
-  [ent]
-  (when (contains? (:block/type ent) "closed value")
-    (:block/content ent)))

+ 19 - 19
src/main/frontend/handler/editor.cljs

@@ -7,33 +7,35 @@
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.db :as db]
+            [frontend.db.async :as db-async]
             [frontend.db.model :as db-model]
+            [frontend.db.query-dsl :as query-dsl]
             [frontend.db.utils :as db-utils]
-            [frontend.db.async :as db-async]
             [frontend.diff :as diff]
+            [frontend.extensions.pdf.utils :as pdf-utils]
             [frontend.format.block :as block]
             [frontend.format.mldoc :as mldoc]
             [frontend.fs :as fs]
-            [logseq.common.path :as path]
-            [frontend.extensions.pdf.utils :as pdf-utils]
+            [frontend.fs.capacitor-fs :as capacitor-fs]
             [frontend.handler.assets :as assets-handler]
             [frontend.handler.block :as block-handler]
             [frontend.handler.common :as common-handler]
-            [frontend.handler.property :as property-handler]
-            [frontend.handler.property.util :as pu]
+            [frontend.handler.db-based.editor :as db-editor-handler]
             [frontend.handler.db-based.property.util :as db-pu]
             [frontend.handler.export.html :as export-html]
             [frontend.handler.export.text :as export-text]
+            [frontend.handler.file-based.editor :as file-editor-handler]
+            [frontend.handler.file-based.status :as status]
             [frontend.handler.notification :as notification]
+            [frontend.handler.property :as property-handler]
+            [frontend.handler.property.file :as property-file]
+            [frontend.handler.property.util :as pu]
             [frontend.handler.repeated :as repeated]
             [frontend.handler.route :as route-handler]
-            [frontend.handler.db-based.editor :as db-editor-handler]
-            [frontend.handler.file-based.editor :as file-editor-handler]
             [frontend.mobile.util :as mobile-util]
-            [logseq.outliner.core :as outliner-core]
             [frontend.modules.outliner.op :as outliner-op]
-            [frontend.modules.outliner.ui :as ui-outliner-tx]
             [frontend.modules.outliner.tree :as tree]
+            [frontend.modules.outliner.ui :as ui-outliner-tx]
             [frontend.search :as search]
             [frontend.state :as state]
             [frontend.template :as template]
@@ -43,30 +45,28 @@
             [frontend.util.drawer :as drawer]
             [frontend.util.keycode :as keycode]
             [frontend.util.list :as list]
-            [frontend.handler.file-based.status :as status]
-            [frontend.handler.property.file :as property-file]
             [frontend.util.text :as text-util]
             [frontend.util.thingatpt :as thingatpt]
+            [goog.crypt.base64 :as base64]
             [goog.dom :as gdom]
             [goog.dom.classes :as gdom-classes]
             [goog.object :as gobj]
-            [goog.crypt.base64 :as base64]
             [goog.string :as gstring]
             [lambdaisland.glogi :as log]
+            [logseq.common.path :as path]
+            [logseq.common.util :as common-util]
+            [logseq.common.util.block-ref :as block-ref]
+            [logseq.common.util.page-ref :as page-ref]
+            [logseq.db :as ldb]
             [logseq.db.frontend.schema :as db-schema]
             [logseq.graph-parser.block :as gp-block]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.text :as text]
             [logseq.graph-parser.utf8 :as utf8]
-            [logseq.common.util :as common-util]
-            [logseq.common.util.block-ref :as block-ref]
-            [logseq.common.util.page-ref :as page-ref]
+            [logseq.outliner.core :as outliner-core]
             [promesa.core :as p]
-            [rum.core :as rum]
-            [frontend.fs.capacitor-fs :as capacitor-fs]
-            [logseq.db :as ldb]
-            [frontend.db.query-dsl :as query-dsl]))
+            [rum.core :as rum]))
 
 ;; FIXME: should support multiple images concurrently uploading
 

+ 7 - 7
src/main/frontend/handler/property.cljs

@@ -10,16 +10,16 @@
   [repo block-id property-id-or-key]
   (if (config/db-based-graph? repo)
     (let [eid (if (uuid? block-id) [:block/uuid block-id] block-id)]
-      (db-property-handler/remove-block-property! repo eid property-id-or-key))
+      (db-property-handler/remove-block-property! eid property-id-or-key))
     (file-property-handler/remove-block-property! block-id property-id-or-key)))
 
 (defn set-block-property!
-  [repo block-id key v & opts]
+  [repo block-id key v]
   (if (config/db-based-graph? repo)
     (let [eid (if (uuid? block-id) [:block/uuid block-id] block-id)]
       (if (or (nil? v) (and (coll? v) (empty? v)))
-       (db-property-handler/remove-block-property! repo eid key)
-       (db-property-handler/set-block-property! repo eid key v opts)))
+        (db-property-handler/remove-block-property! eid key)
+        (db-property-handler/set-block-property! eid key v)))
     (file-property-handler/set-block-property! block-id key v)))
 
 (defn add-page-property!
@@ -56,13 +56,13 @@
 (defn batch-remove-block-property!
   [repo block-ids key]
   (if (config/db-based-graph? repo)
-    (db-property-handler/batch-remove-property! repo block-ids key)
+    (db-property-handler/batch-remove-property! block-ids key)
     (file-property-handler/batch-remove-block-property! block-ids key)))
 
 (defn batch-set-block-property!
   [repo block-ids key value]
   (if (config/db-based-graph? repo)
     (if (nil? value)
-      (db-property-handler/batch-remove-property! repo block-ids key)
-      (db-property-handler/batch-set-property! repo block-ids key value))
+      (db-property-handler/batch-remove-property! block-ids key)
+      (db-property-handler/batch-set-property! block-ids key value))
     (file-property-handler/batch-set-block-property! block-ids key value)))

+ 0 - 6
src/main/frontend/handler/property/util.cljs

@@ -55,9 +55,3 @@
   (let [repo (state/get-current-repo)
         db (db/get-db repo)]
     (db-property/get-closed-property-values db property-id)))
-
-(defn get-closed-value-entity-by-name
-  [property-id value-name]
-  (let [repo (state/get-current-repo)
-        db (db/get-db repo)]
-    (db-property/get-closed-value-entity-by-name db property-id value-name)))

+ 90 - 45
src/main/frontend/modules/outliner/op.cljs

@@ -7,67 +7,112 @@
   nil)
 
 (defn- op-transact!
-  [fn-var & args]
-  {:pre [(var? fn-var)]}
+  [result]
   (when (nil? *outliner-ops*)
-    (throw (js/Error. (str (:name (meta fn-var)) " is not used in (transact! ...)"))))
-  (let [result (apply @fn-var args)]
-    (conj! *outliner-ops* result)
-    result))
-
-(defn save-block
-  [block opts]
-  (when-let [block' (if (de/entity? block)
-                      (assoc (.-kv ^js block) :db/id (:db/id block))
-                      block)]
-    [:save-block [block' opts]]))
-
-(defn insert-blocks
-  [blocks target-block opts]
-  (let [id (:db/id target-block)]
-    [:insert-blocks [blocks id opts]]))
-
-(defn delete-blocks
-  [blocks opts]
-  (let [ids (map :db/id blocks)]
-    [:delete-blocks [ids opts]]))
-
-(defn move-blocks
-  [blocks target-block sibling?]
-  (let [ids (map :db/id blocks)
-        target-id (:db/id target-block)]
-    [:move-blocks [ids target-id sibling?]]))
-
-(defn move-blocks-up-down
-  [blocks up?]
-  (let [ids (map :db/id blocks)]
-    [:move-blocks-up-down [ids up?]]))
-
-(defn indent-outdent-blocks
-  [blocks indent? & {:as opts}]
-  (let [ids (map :db/id blocks)]
-    [:indent-outdent-blocks [ids indent? opts]]))
+    (throw (js/Error. (str (name (first result)) " not used in (transact! ...)"))))
+  (conj! *outliner-ops* result)
+  result)
 
 (defn save-block!
   [block & {:as opts}]
-  (op-transact! #'save-block block opts))
+  (op-transact!
+   (when-let [block' (if (de/entity? block)
+                       (assoc (.-kv ^js block) :db/id (:db/id block))
+                       block)]
+     [:save-block [block' opts]])))
 
 (defn insert-blocks!
   [blocks target-block opts]
-  (op-transact! #'insert-blocks blocks target-block opts))
+  (op-transact!
+   (let [id (:db/id target-block)]
+    [:insert-blocks [blocks id opts]])))
 
 (defn delete-blocks!
   [blocks opts]
-  (op-transact! #'delete-blocks blocks opts))
+  (op-transact!
+   (let [ids (map :db/id blocks)]
+    [:delete-blocks [ids opts]])))
 
 (defn move-blocks!
   [blocks target-block sibling?]
-  (op-transact! #'move-blocks blocks target-block sibling?))
+  (op-transact!
+   (let [ids (map :db/id blocks)
+        target-id (:db/id target-block)]
+    [:move-blocks [ids target-id sibling?]])))
 
 (defn move-blocks-up-down!
   [blocks up?]
-  (op-transact! #'move-blocks-up-down blocks up?))
+  (op-transact!
+   (let [ids (map :db/id blocks)]
+    [:move-blocks-up-down [ids up?]])))
 
 (defn indent-outdent-blocks!
   [blocks indent? & {:as opts}]
-  (op-transact! #'indent-outdent-blocks blocks indent? opts))
+  (op-transact!
+   (let [ids (map :db/id blocks)]
+    [:indent-outdent-blocks [ids indent? opts]])))
+
+(defn upsert-property!
+  [property-id schema property-opts]
+  (op-transact!
+   [:upsert-property [property-id schema property-opts]]))
+
+(defn set-block-property!
+  [block-eid property-id value]
+  (op-transact!
+   [:set-block-property [block-eid property-id value]]))
+
+(defn remove-block-property!
+  [block-eid property-id]
+  (op-transact!
+   [:remove-block-property [block-eid property-id]]))
+
+(defn delete-property-value!
+  [block-eid property-id property-value]
+  (op-transact!
+   [:delete-property-value [block-eid property-id property-value]]))
+
+(defn create-property-text-block!
+  [block-id property-id value opts]
+  (op-transact!
+   [:create-property-text-block [block-id property-id value opts]]))
+
+(defn collapse-expand-block-property!
+  [block-id property-id collapse?]
+  (op-transact!
+   [:collapse-expand-block-property [block-id property-id collapse?]]))
+
+(defn batch-set-property!
+  [block-ids property-id value]
+  (op-transact!
+   [:batch-set-property [block-ids property-id value]]))
+
+(defn batch-remove-property!
+  [block-ids property-id]
+  (op-transact!
+   [:batch-remove-property [block-ids property-id]]))
+
+(defn class-add-property!
+  [class-id property-id]
+  (op-transact!
+   [:class-add-property [class-id property-id]]))
+
+(defn class-remove-property!
+  [class-id property-id]
+  (op-transact!
+   [:class-remove-property [class-id property-id]]))
+
+(defn upsert-closed-value!
+  [property-id closed-value-config]
+  (op-transact!
+   [:upsert-closed-value [property-id closed-value-config]]))
+
+(defn delete-closed-value!
+  [property-id value-block-id]
+  (op-transact!
+   [:delete-closed-value [property-id value-block-id]]))
+
+(defn add-existing-values-to-closed-values!
+  [property-id values]
+  (op-transact!
+   [:add-existing-values-to-closed-values [property-id values]]))

+ 4 - 3
src/main/frontend/search.cljs

@@ -17,7 +17,8 @@
             [frontend.db :as db]
             [frontend.db.model :as db-model]
             [frontend.db.utils :as db-utils]
-            [logseq.db :as ldb]))
+            [logseq.db :as ldb]
+            [datascript.core :as d]))
 
 (def fuzzy-search fuzzy/fuzzy-search)
 
@@ -137,7 +138,7 @@
   (when-let [repo (state/get-current-repo)]
     (p/let [page (db/entity page-id)
             alias-names (conj (set (map util/safe-page-name-sanity-lc
-                                     (db/get-page-alias-names repo page-id)))
+                                        (db/get-page-alias-names repo page-id)))
                               (:block/original-name page))
             q (string/join " " alias-names)
             result (block-search repo q {:limit 100})
@@ -145,7 +146,7 @@
             result (when (seq eids)
                      (.get-page-unlinked-refs ^Object @state/*db-worker repo (:db/id page) (ldb/write-transit-str eids)))
             result' (when result (ldb/read-transit-str result))]
-      (when result' (db/transact! repo result'))
+      (when result' (d/transact! (db/get-db repo false) result'))
       (some->> result'
                db-model/sort-by-order-recursive
                db-utils/group-by-page))))

+ 0 - 10
src/main/frontend/util.cljc

@@ -1496,16 +1496,6 @@ Arg *stop: atom, reset to true to stop the loop"
                   js/window.msRequestAnimationFrame))
          #(js/setTimeout % 16))))
 
-#?(:cljs
-   (defn tag?
-     "Whether `s` is a tag."
-     [s]
-     (and (string? s)
-          (string/starts-with? s "#")
-          (or
-           (not (string/includes? s " "))
-           (string/starts-with? s "#[[")
-           (string/ends-with? s "]]")))))
 #?(:cljs
    (defn parse-params
      "Parse URL parameters in hash(fragment) into a hashmap"

+ 21 - 15
src/test/frontend/db/db_based_model_test.cljs

@@ -4,7 +4,7 @@
             [frontend.db :as db]
             [frontend.test.helper :as test-helper]
             [datascript.core :as d]
-            [frontend.handler.db-based.property :as db-property-handler]
+            [logseq.outliner.property :as outliner-property]
             [frontend.handler.page :as page-handler]
             [logseq.db :as ldb]))
 
@@ -23,15 +23,20 @@
 (use-fixtures :each start-and-destroy-db)
 
 (deftest get-block-property-values-test
-  (db-property-handler/set-block-property! repo fbid :user.property/property-1 "value 1" {:property-type :string})
-  (db-property-handler/set-block-property! repo sbid :user.property/property-1 "value 2" {:property-type :string})
-  (is (= (model/get-block-property-values :user.property/property-1)
-         ["value 1" "value 2"])))
+  (let [conn (db/get-db false)]
+    (outliner-property/upsert-property! conn :user.property/property-1 {:type :string} {:property-name "property 1"})
+    (outliner-property/set-block-property! conn fbid :user.property/property-1 "value 1")
+    (outliner-property/set-block-property! conn sbid :user.property/property-1 "value 2")
+    (is (= (model/get-block-property-values :user.property/property-1)
+           ["value 1" "value 2"]))))
 
 (deftest get-db-property-values-test
-  (db-property-handler/set-block-property! repo fbid :user.property/property-1 "1" {})
-  (db-property-handler/set-block-property! repo sbid :user.property/property-1 "2" {})
-  (is (= [1 2] (model/get-block-property-values :user.property/property-1))))
+  (let [conn (db/get-db false)]
+    (outliner-property/upsert-property! conn :user.property/property-1 {:type :number} {:property-name "property 1"})
+    (outliner-property/set-block-property! conn fbid :user.property/property-1 "1")
+    (outliner-property/set-block-property! conn sbid :user.property/property-1 "2")
+    (is (= ["1" "2"]
+           (map (fn [id] (:block/content (d/entity @conn id))) (model/get-block-property-values :user.property/property-1))))))
 
 ;; (deftest get-db-property-values-test-with-pages
 ;;   (let [opts {:redirect? false :create-first-block? false}
@@ -39,9 +44,9 @@
 ;;         _ (page-handler/create! "page2" opts)
 ;;         p1id (:block/uuid (db/get-page "page1"))
 ;;         p2id (:block/uuid (db/get-page "page2"))]
-;;     (db-property-handler/upsert-property! repo "property-1" {:type :page} {})
-;;     (db-property-handler/set-block-property! repo fbid "property-1" p1id {})
-;;     (db-property-handler/set-block-property! repo sbid "property-1" p2id {})
+;;     (outliner-property/upsert-property! repo "property-1" {:type :page} {})
+;;     (outliner-property/set-block-property! repo fbid "property-1" p1id {})
+;;     (outliner-property/set-block-property! repo sbid "property-1" p2id {})
 ;;     (is (= '("[[page1]]" "[[page2]]") (model/get-db-property-values repo "property-1")))))
 
 (deftest get-all-classes-test
@@ -74,10 +79,11 @@
         _ (page-handler/create! "class1" opts)
         _ (page-handler/create! "class2" opts)
         class1 (db/get-page "class1")
-        class2 (db/get-page "class2")]
-    (db-property-handler/upsert-property! repo :user.property/property-1 {:type :page} {})
-    (db-property-handler/class-add-property! repo (:block/uuid class1) :user.property/property-1)
-    (db-property-handler/class-add-property! repo (:block/uuid class2) :user.property/property-1)
+        class2 (db/get-page "class2")
+        conn (db/get-db false)]
+    (outliner-property/upsert-property! conn :user.property/property-1 {:type :page} {})
+    (outliner-property/class-add-property! conn (:db/id class1) :user.property/property-1)
+    (outliner-property/class-add-property! conn (:db/id class2) :user.property/property-1)
     (let [property (db/entity :user.property/property-1)
           classes (model/get-classes-with-property (:db/ident property))]
       (is (= (set (map :db/id classes))

+ 0 - 61
src/test/frontend/handler/db_based/property_async_test.cljs

@@ -1,61 +0,0 @@
-(ns frontend.handler.db-based.property-async-test
-  (:require [frontend.handler.db-based.property :as db-property-handler]
-            [frontend.db :as db]
-            [clojure.test :refer [is testing async use-fixtures]]
-            [frontend.test.helper :as test-helper :include-macros true :refer [deftest-async]]
-            [datascript.core :as d]
-            [promesa.core :as p]
-            [frontend.db.conn :as conn]
-            [frontend.state :as state]
-            [frontend.handler.editor :as editor-handler]))
-
-(def repo test-helper/test-db-name-db-version)
-
-(def init-data (test-helper/initial-test-page-and-blocks))
-
-(def fbid (:block/uuid (second init-data)))
-
-(use-fixtures :each
-  {:before (fn []
-             (async done
-                    (test-helper/start-test-db! {:db-graph? true})
-                    (p/let [_tx-report (d/transact! (conn/get-db repo false) init-data)]
-                      (done))))
-   :after test-helper/destroy-test-db!})
-
-;; property-create-new-block
-;; get-property-block-created-block
-(deftest-async text-block-test
-  (testing "Add property and create a block value"
-    (let [repo (state/get-current-repo)
-          fb (db/entity [:block/uuid fbid])
-          k :user.property/property-1]
-      (p/do!
-       ;; add property
-       (db-property-handler/upsert-property! repo k {:type :default} {})
-       (p/let [property (db/entity k)
-               {:keys [block-id]} (db-property-handler/create-property-text-block! fb property "Block content" editor-handler/wrap-parse-block {})
-               {:keys [from-property-id]} (db-property-handler/get-property-block-created-block [:block/uuid block-id])]
-         (is (= from-property-id (:db/id property))))))))
-
-;; collapse-expand-property!
-(deftest-async collapse-expand-property-test
-  (testing "Collapse and expand property"
-    (let [repo (state/get-current-repo)
-          fb (db/entity [:block/uuid fbid])
-          k :user.property/property-1]
-      (p/do!
-       ;; add property
-       (db-property-handler/upsert-property! repo k {:type :default} {})
-       (let [property (db/entity k)]
-         (p/do!
-          (db-property-handler/create-property-text-block! fb property "Block content" editor-handler/wrap-parse-block {})
-            ;; collapse property-1
-          (db-property-handler/collapse-expand-property! repo fb property true)
-          (is (=
-               [(:db/id property)]
-               (map :db/id (:block/collapsed-properties (db/entity [:block/uuid fbid])))))
-
-            ;; expand property-1
-          (db-property-handler/collapse-expand-property! repo fb property false)
-          (is (nil? (:block/collapsed-properties (db/entity [:block/uuid fbid]))))))))))

+ 0 - 87
src/test/frontend/handler/db_based/property_closed_value_test.cljs

@@ -1,87 +0,0 @@
-(ns frontend.handler.db-based.property-closed-value-test
-  (:require [frontend.handler.db-based.property :as db-property-handler]
-            [frontend.db :as db]
-            [clojure.test :refer [is testing async use-fixtures]]
-            [frontend.test.helper :as test-helper :include-macros true :refer [deftest-async]]
-            [datascript.core :as d]
-            [promesa.core :as p]
-            [frontend.db.conn :as conn]))
-
-(def repo test-helper/test-db-name-db-version)
-
-(def init-data (test-helper/initial-test-page-and-blocks))
-
-;; init page id
-;; (def pid (:block/uuid (first init-data)))
-;; first block id
-(def fbid (:block/uuid (second init-data)))
-(def sbid (:block/uuid (nth init-data 2)))
-
-(use-fixtures :once
-  {:before (fn []
-             (async done
-                    (test-helper/start-test-db! {:db-graph? true})
-                    (p/let [_ (d/transact! (conn/get-db repo false) init-data)]
-                      (done))))
-   :after test-helper/destroy-test-db!})
-
-(defn- get-value-ids
-  [k]
-  (map :block/uuid (:property/closed-values (db/entity k))))
-
-(defn- get-closed-values
-  "Get value from block ids"
-  [values]
-  (set (map #(:block/content (db/entity [:block/uuid %])) values)))
-
-;; closed values related
-;; upsert-closed-value
-;; add-existing-values-to-closed-values!
-;; delete-closed-value
-(deftest-async closed-values-test
-  (testing "Create properties and closed values"
-    (db-property-handler/set-block-property! repo fbid :user.property/property-1 "1" {})
-    (db-property-handler/set-block-property! repo sbid :user.property/property-1 "2" {})
-    (let [k :user.property/property-1
-          property (db/entity k)]
-      (p/do!
-       (db-property-handler/<add-existing-values-to-closed-values! property [1 2])
-       (testing "Add existing values to closed values"
-         (let [values (get-value-ids k)]
-           (is (every? uuid? values))
-           (is (= #{1 2} (get-closed-values values)))
-           (is (every? #(contains? (:block/type (db/entity [:block/uuid %])) "closed value")
-                       values))))
-       (testing "Add non-numbers shouldn't work"
-         (p/let [result (db-property-handler/<upsert-closed-value property {:value "not a number"})]
-           (is (= result :value-invalid))
-           (let [values (get-value-ids k)]
-             (is (= #{1 2} (get-closed-values values))))))
-
-       (testing "Add existing value"
-         (p/let [result (db-property-handler/<upsert-closed-value property {:value 2})]
-           (is (= result :value-exists))))
-
-       (testing "Add new value"
-         (p/let [{:keys [block-id tx-data]} (db-property-handler/<upsert-closed-value property {:value 3})]
-           (db/transact! tx-data)
-           (let [b (db/entity [:block/uuid block-id])]
-             (is (= 3 (:block/content b)))
-             (is (contains? (:block/type b) "closed value"))
-             (let [values (get-value-ids k)]
-               (is (= #{1 2 3} (get-closed-values values))))
-
-             (testing "Update closed value"
-               (p/let [{:keys [tx-data]} (db-property-handler/<upsert-closed-value property {:id block-id
-                                                                                             :value 4
-                                                                                             :description "choice 4"})]
-                 (db/transact! tx-data)
-                 (let [b (db/entity [:block/uuid block-id])]
-                   (is (= 4 (:block/content b)))
-                   (is (= "choice 4" (:description (:block/schema b))))
-                   (is (contains? (:block/type b) "closed value"))
-                   (p/let [_ (db-property-handler/delete-closed-value! property (db/entity [:block/uuid block-id]))]
-
-                     (testing "Delete closed value"
-                       (is (nil? (db/entity [:block/uuid block-id])))
-                       (is (= 2 (count (:property/closed-values (db/entity k)))))))))))))))))

+ 174 - 54
src/test/frontend/handler/db_based/property_test.cljs

@@ -1,5 +1,5 @@
 (ns frontend.handler.db-based.property-test
-  (:require [frontend.handler.db-based.property :as db-property-handler]
+  (:require [logseq.outliner.property :as outliner-property]
             [frontend.db :as db]
             [clojure.test :refer [deftest is testing are use-fixtures]]
             [frontend.test.helper :as test-helper]
@@ -34,7 +34,10 @@
 ;; update-property!
 (deftest ^:large-vars/cleanup-todo block-property-test
   (testing "Add a property to a block"
-    (db-property-handler/set-block-property! repo fbid :user.property/property-1 "value" {:property-type :string})
+    (let [conn (db/get-db false)]
+      (outliner-property/upsert-property! conn :user.property/property-1 {:type :string}
+                                          {:property-name "property 1"})
+      (outliner-property/set-block-property! conn fbid :user.property/property-1 "value"))
     (let [block (db/entity [:block/uuid fbid])
           properties (:block/properties block)
           property (db/entity :user.property/property-1)]
@@ -54,7 +57,11 @@
         "value")))
 
   (testing "Add another property"
-    (db-property-handler/set-block-property! repo fbid :user.property/property-2 "1" {})
+    (let [conn (db/get-db false)]
+      (outliner-property/upsert-property! conn :user.property/property-2 {:type :number}
+                                          {:property-name "property 2"})
+      (outliner-property/set-block-property! conn fbid :user.property/property-2 "1"))
+
     (let [block (db/entity [:block/uuid fbid])
           properties (:block/properties block)
           property (db/entity :user.property/property-2)]
@@ -70,54 +77,58 @@
         2
         (every? keyword? (map first properties))
         true
-        (second (second properties))
-        1)))
+        (:block/content (second (second properties)))
+        "1")))
 
   (testing "Update property value"
-    (db-property-handler/set-block-property! repo fbid :user.property/property-2 2 {})
+    (let [conn (db/get-db false)]
+      (outliner-property/set-block-property! conn fbid :user.property/property-2 2))
     (let [block (db/entity [:block/uuid fbid])
           properties (:block/properties block)]
       ;; check block's properties
       (are [x y] (= x y)
         (count properties)
         2
-        (second (second properties))
-        2)))
+        (:block/content (second (second properties)))
+        "2")))
 
   (testing "Wrong type property value shouldn't transacted"
-    (db-property-handler/set-block-property! repo fbid :user.property/property-2 "Not a number" {})
+    (let [conn (db/get-db false)]
+      (outliner-property/set-block-property! conn fbid :user.property/property-2 "Not a number"))
     (let [block (db/entity [:block/uuid fbid])
           properties (:block/properties block)]
       ;; check block's properties
       (are [x y] (= x y)
         (count properties)
         2
-        (second (second properties))
-        2)))
+        (:block/content (second (second properties)))
+        "2")))
 
   (testing "Add a multi-values property"
-    (db-property-handler/upsert-property! repo :user.property/property-3 {:type :number :cardinality :many} {})
-    (db-property-handler/set-block-property! repo fbid :user.property/property-3 1 {})
-    (db-property-handler/set-block-property! repo fbid :user.property/property-3 2 {})
-    (db-property-handler/set-block-property! repo fbid :user.property/property-3 3 {})
-    (let [block (db/entity [:block/uuid fbid])
-          properties (:block/properties block)
-          property (db/entity :user.property/property-3)]
+    (let [conn (db/get-db false)]
+      (outliner-property/upsert-property! conn :user.property/property-3 {:type :number :cardinality :many}
+                                          {:property-name "property 3"})
+      (outliner-property/set-block-property! conn fbid :user.property/property-3 1)
+      (outliner-property/set-block-property! conn fbid :user.property/property-3 2)
+      (outliner-property/set-block-property! conn fbid :user.property/property-3 3)
+      (let [block (db/entity [:block/uuid fbid])
+            properties (:block/properties block)
+            property (db/entity :user.property/property-3)]
       ;; ensure property exists
-      (are [x y] (= x y)
-        (:block/schema property)
-        {:type :number}
-        (:block/type property)
-        #{"property"})
+        (are [x y] (= x y)
+          (:block/schema property)
+          {:type :number}
+          (:block/type property)
+          #{"property"})
       ;; check block's properties
-      (are [x y] (= x y)
-        3
-        (count properties)
-        #{1 2 3}
-        (get properties :user.property/property-3))))
+        (are [x y] (= x y)
+          3
+          (count properties)
+          #{"1" "2" "3"}
+          (set (map :block/content (get properties :user.property/property-3)))))))
 
   (testing "Remove a property"
-    (db-property-handler/remove-block-property! repo fbid :user.property/property-3)
+    (outliner-property/remove-block-property! (db/get-db false) fbid :user.property/property-3)
     (let [block (db/entity [:block/uuid fbid])
           properties (:block/properties block)]
       ;; check block's properties
@@ -129,9 +140,10 @@
 
   (testing "Batch set properties"
     (let [k :user.property/property-4
-          v "batch value"]
-      (db-property-handler/upsert-property! repo :user.property/property-4 {:type :string} {})
-      (db-property-handler/batch-set-property! repo [fbid sbid] k v)
+          v "batch value"
+          conn (db/get-db false)]
+      (outliner-property/upsert-property! conn :user.property/property-4 {:type :string} {})
+      (outliner-property/batch-set-property! conn [fbid sbid] k v)
       (let [fb (db/entity [:block/uuid fbid])
             sb (db/entity [:block/uuid sbid])]
         (are [x y] (= x y)
@@ -142,7 +154,7 @@
 
   (testing "Batch remove properties"
     (let [k :user.property/property-4]
-      (db-property-handler/batch-remove-property! repo [fbid sbid] k)
+      (outliner-property/batch-remove-property! (db/get-db false) [fbid sbid] k)
       (let [fb (db/entity [:block/uuid fbid])
             sb (db/entity [:block/uuid sbid])]
         (are [x y] (= x y)
@@ -162,8 +174,8 @@
         _ (page-handler/create! "class3" opts)
         c1 (db/get-page "class1")
         c2 (db/get-page "class2")
-        c1id (:block/uuid c1)
-        c2id (:block/uuid c2)]
+        c1id (:db/id c1)
+        c2id (:db/id c2)]
 
     (testing "Create classes"
       (are [x y] (= x y)
@@ -173,16 +185,18 @@
         #{"class"}))
 
     (testing "Class add property"
-      (db-property-handler/class-add-property! repo c1id :user.property/property-1)
-      (db-property-handler/class-add-property! repo c1id :user.property/property-2)
+      (let [conn (db/get-db false)]
+        (outliner-property/class-add-property! conn c1id :user.property/property-1)
+        (outliner-property/class-add-property! conn c1id :user.property/property-2)
       ;; repeated adding property-2
-      (db-property-handler/class-add-property! repo c1id :user.property/property-2)
+        (outliner-property/class-add-property! conn c1id :user.property/property-2)
       ;; add new property with same base db-ident as property-1
-      (db-property-handler/class-add-property! repo c1id ":property-1")
-      (is (= 3 (count (:class/schema.properties (db/entity (:db/id c1)))))))
+        (outliner-property/class-add-property! conn c1id ":property-1")
+        (is (= 3 (count (:class/schema.properties (db/entity (:db/id c1))))))))
 
     (testing "Class remove property"
-      (db-property-handler/class-remove-property! repo c1id :user.property/property-1)
+      (let [conn (db/get-db false)]
+        (outliner-property/class-remove-property! conn c1id :user.property/property-1))
       (is (= 2 (count (:class/schema.properties (db/entity (:db/id c1)))))))
     (testing "Add classes to a block"
       (test-helper/save-block! repo fbid "Block 1" {:tags ["class1" "class2" "class3"]})
@@ -195,13 +209,14 @@
         (is (= 2 (count (:block/tags (db/entity [:block/uuid fbid]))))))
     (testing "Get block's classes properties"
       ;; set c2 as parent of c3
-      (let [c3 (db/get-page "class3")]
+      (let [c3 (db/get-page "class3")
+            conn (db/get-db false)]
         (db/transact! [{:db/id (:db/id c3)
-                        :class/parent (:db/id c2)}]))
-      (db-property-handler/class-add-property! repo c2id :user.property/property-3)
-      (db-property-handler/class-add-property! repo c2id :user.property/property-4)
+                        :class/parent (:db/id c2)}])
+        (outliner-property/class-add-property! conn c2id :user.property/property-3)
+        (outliner-property/class-add-property! conn c2id :user.property/property-4)
       (is (= 4 (count (:classes-properties
-                       (db-property-handler/get-block-classes-properties (:db/id (db/entity [:block/uuid fbid]))))))))))
+                       (outliner-property/get-block-classes-properties @conn (:db/id (db/entity [:block/uuid fbid])))))))))))
 
 
 ;; convert-property-input-string
@@ -210,7 +225,7 @@
     (let [test-uuid (random-uuid)]
       (are [x y]
            (= (let [[schema-type value] x]
-                (db-property-handler/convert-property-input-string schema-type value)) y)
+                (outliner-property/convert-property-input-string schema-type value)) y)
         [:number "1"] 1
         [:number "1.2"] 1.2
         [:url test-uuid] test-uuid
@@ -220,15 +235,17 @@
 
 (deftest upsert-property!
   (testing "Update an existing property"
-    (let [repo (state/get-current-repo)]
-      (db-property-handler/upsert-property! repo nil {:type :default} {:property-name "p0"})
-      (db-property-handler/upsert-property! repo :user.property/p0 {:type :default :cardinality :many} {})
+    (let [repo (state/get-current-repo)
+          conn (db/get-db false)]
+      (outliner-property/upsert-property! conn nil {:type :default} {:property-name "p0"})
+      (outliner-property/upsert-property! conn :user.property/p0 {:type :default :cardinality :many} {})
       (is (db-property/many? (db/entity repo :user.property/p0)))))
   (testing "Multiple properties that generate the same initial :db/ident"
-    (let [repo (state/get-current-repo)]
-      (db-property-handler/upsert-property! repo nil {:type :default} {:property-name "p1"})
-      (db-property-handler/upsert-property! repo nil {} {:property-name ":p1"})
-      (db-property-handler/upsert-property! repo nil {} {:property-name "1p1"})
+    (let [repo (state/get-current-repo)
+          conn (db/get-db false)]
+      (outliner-property/upsert-property! conn nil {:type :default} {:property-name "p1"})
+      (outliner-property/upsert-property! conn nil {} {:property-name ":p1"})
+      (outliner-property/upsert-property! conn nil {} {:property-name "1p1"})
 
       (is (= {:block/name "p1" :block/original-name "p1" :block/schema {:type :default}}
              (select-keys (db/entity repo :user.property/p1) [:block/name :block/original-name :block/schema]))
@@ -243,4 +260,107 @@
 ;; template (TBD, template implementation not settle down yet)
 ;; property-create-new-block-from-template
 
+(defn- get-value-ids
+  [k]
+  (map :block/uuid (:property/closed-values (db/entity k))))
+
+(defn- get-closed-values
+  "Get value from block ids"
+  [values]
+  (set (map #(:block/content (db/entity [:block/uuid %])) values)))
+
+;; closed values related
+;; upsert-closed-value
+;; add-existing-values-to-closed-values!
+;; delete-closed-value
+(deftest closed-values-test
+  (testing "Create properties and closed values"
+    (let [conn (db/get-db false)]
+      (outliner-property/upsert-property! conn :user.property/property-1 {:type :number} {})
+      (outliner-property/set-block-property! conn fbid :user.property/property-1 "1")
+      (outliner-property/set-block-property! conn sbid :user.property/property-1 "2"))
+    (let [k :user.property/property-1
+          property (db/entity k)
+          conn (db/get-db false)
+          values (map (fn [d] (:block/uuid (d/entity @conn (:v d)))) (d/datoms @conn :avet :user.property/property-1))]
+      (outliner-property/add-existing-values-to-closed-values! conn (:db/id property) values)
+      (testing "Add existing values to closed values"
+        (let [values (get-value-ids k)]
+          (is (every? uuid? values))
+          (is (= #{"1" "2"} (get-closed-values values)))
+          (is (every? #(contains? (:block/type (db/entity [:block/uuid %])) "closed value")
+                      values))))
+      (testing "Add non-numbers shouldn't work"
+        (let [result (outliner-property/upsert-closed-value! conn (:db/id property) {:value "not a number"})]
+          (is (= result :value-invalid))
+          (let [values (get-value-ids k)]
+            (is (= #{"1" "2"} (get-closed-values values))))))
+
+      (testing "Add existing value"
+        (let [result (outliner-property/upsert-closed-value! conn (:db/id property) {:value 2})]
+          (is (= result :value-exists))))
+
+      (testing "Add new value"
+        (let [{:keys [block-id tx-data]} (outliner-property/upsert-closed-value! conn (:db/id property) {:value 3})]
+          (db/transact! tx-data)
+          (let [b (db/entity [:block/uuid block-id])]
+            (is (= "3" (:block/content b)))
+            (is (contains? (:block/type b) "closed value"))
+            (let [values (get-value-ids k)]
+              (is (= #{"1" "2" "3"}
+                     (get-closed-values values))))
+
+            (testing "Update closed value"
+              (let [{:keys [tx-data]} (outliner-property/upsert-closed-value! conn (:db/id property) {:id block-id
+                                                                                                      :value 4
+                                                                                                      :description "choice 4"})]
+                (db/transact! tx-data)
+                (let [b (db/entity [:block/uuid block-id])]
+                  (is (= "4" (:block/content b)))
+                  (is (= "choice 4" (:description (:block/schema b))))
+                  (is (contains? (:block/type b) "closed value"))
+                  (outliner-property/delete-closed-value! conn (:db/id property) (:db/id (db/entity [:block/uuid block-id])))
+                  (testing "Delete closed value"
+                    (is (nil? (db/entity [:block/uuid block-id])))
+                    (is (= 2 (count (:property/closed-values (db/entity k)))))))))))))))
+
+;; property-create-new-block
+;; get-property-block-created-block
+(deftest text-block-test
+  (testing "Add property and create a block value"
+    (let [fb (db/entity [:block/uuid fbid])
+          k :user.property/property-1
+          conn (db/get-db false)]
+      ;; add property
+      (outliner-property/upsert-property! conn k {:type :default} {})
+      (let [property (db/entity k)
+            block-id (outliner-property/create-property-text-block! conn (:db/id fb) (:db/id property) "Block content" {})
+            {:keys [from-property-id]} (outliner-property/get-property-block-created-block @conn [:block/uuid block-id])]
+        (is (= from-property-id (:db/id property)))))))
+
+;; collapse-expand-property!
+(deftest collapse-expand-property-test
+  (testing "Collapse and expand property"
+    (let [conn (db/get-db false)
+          fb (db/entity [:block/uuid fbid])
+          k :user.property/property-1]
+      ;; add property
+      (outliner-property/upsert-property! conn k {:type :default} {})
+      (let [property (db/entity k)]
+        (outliner-property/create-property-text-block! conn
+                                                       (:db/id fb)
+                                                       (:db/id property)
+                                                       "Block content"
+                                                       {})
+            ;; collapse property-1
+        (outliner-property/collapse-expand-block-property! conn (:db/id fb) (:db/id property) true)
+        (is (=
+             [(:db/id property)]
+             (map :db/id (:block/collapsed-properties (db/entity [:block/uuid fbid])))))
+
+            ;; expand property-1
+        (outliner-property/collapse-expand-block-property! conn (:db/id fb) (:db/id property) false)
+        (is (nil? (:block/collapsed-properties (db/entity [:block/uuid fbid]))))))))
+
+
 #_(cljs.test/run-tests)