Просмотр исходного кода

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 год назад
Родитель
Сommit
ce4cad2cc7
31 измененных файлов с 1235 добавлено и 1028 удалено
  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)
   (when (string? tag-name)
     (not (re-find #"[#\t\r\n]+" 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
 (defn safe-subs
   ([s start]
   ([s start]
    (let [c (count s)]
    (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?]}]
   [db validate-fn [{:block/keys [schema] :as property} property-val] & {:keys [new-closed-value?]}]
   ;; For debugging
   ;; For debugging
   ;; (when (not= "logseq.property" (namespace (:db/ident property))) (prn :validate-val (dissoc property :property/closed-values) property-val))
   ;; (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))
         validate-fn'' (if (and (db-property-type/closed-value-property-types (:type schema))
                                ;; new closed values aren't associated with the property yet
                                ;; new closed values aren't associated with the property yet
                                (not new-closed-value?)
                                (not new-closed-value?)

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

@@ -310,3 +310,9 @@
 (defn many?
 (defn many?
   [property]
   [property]
   (= (:db/cardinality property) :db.cardinality/many))
   (= (: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]
             [logseq.db.frontend.order :as db-order]
             [datascript.core :as d]))
             [datascript.core :as d]))
 
 
-(defonce hidden-page-name-prefix "$$$")
-
 (defn- closed-value-new-block
 (defn- closed-value-new-block
   [block-id value property]
   [block-id value property]
   (let [property-id (:db/ident property)]
   (let [property-id (:db/ident property)]
@@ -43,7 +41,7 @@
 (defn build-closed-values
 (defn build-closed-values
   "Builds all the tx needed for property with closed values including
   "Builds all the tx needed for property with closed values including
    the hidden page and closed value blocks as needed"
    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)
   (let [property-schema (:block/schema property)
         property-tx (merge (sqlite-util/build-new-property db-ident property-schema {:original-name prop-name
         property-tx (merge (sqlite-util/build-new-property db-ident property-schema {:original-name prop-name
                                                                                      :ref-type? true})
                                                                                      :ref-type? true})
@@ -62,14 +60,14 @@
                          value
                          value
                          property
                          property
                          {:db-ident db-ident :icon icon :description description})
                          {:db-ident db-ident :icon icon :description description})
-                         (not from-ui-thread?)
+                         true
                          (assoc :block/order (db-order/gen-key))))
                          (assoc :block/order (db-order/gen-key))))
                      (:closed-values property))]
                      (:closed-values property))]
             closed-value-blocks-tx))]
             closed-value-blocks-tx))]
     (into [property-tx] hidden-tx)))
     (into [property-tx] hidden-tx)))
 
 
 (defn build-property-value-block
 (defn build-property-value-block
-  [block property value parse-block]
+  [block property value]
   (-> {:block/uuid (d/squuid)
   (-> {:block/uuid (d/squuid)
        :block/format :markdown
        :block/format :markdown
        :block/content value
        :block/content value
@@ -79,6 +77,6 @@
                      (:db/id block))
                      (:db/id block))
        :block/parent (:db/id block)
        :block/parent (:db/id block)
        :logseq.property/created-from-property (or (:db/id property)
        :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
 (def value-ref-property-types
   "Property value ref types"
   "Property value ref types"
-  #{:default :url :number})
+  #{:default :url :number :template})
 
 
 (def ref-property-types
 (def ref-property-types
   "User facing ref types"
   "User facing ref types"
@@ -73,9 +73,25 @@
   (some? (d/entity db id)))
   (some? (d/entity db id)))
 
 
 (defn- url-entity?
 (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?
 (defn- property-value-block?
   [db s]
   [db s]
@@ -94,17 +110,6 @@
     (and (some? (:block/original-name ent))
     (and (some? (:block/original-name ent))
          (contains? (:block/type ent) "journal"))))
          (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
 (def built-in-validation-schemas
   "Map of types to malli validation schemas that validate a property value for that type"
   "Map of types to malli validation schemas that validate a property value for that type"
@@ -113,7 +118,7 @@
               property-value-block?]
               property-value-block?]
    :string   [:fn
    :string   [:fn
               {:error/message "should be a string"}
               {:error/message "should be a string"}
-              string-or-closed-string?]
+              string-entity?]
    :number   [:fn
    :number   [:fn
               {:error/message "should be a number"}
               {:error/message "should be a number"}
               number-entity?]
               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
   "Build a standard new property so that it is is consistent across contexts. Takes
    an optional map with following keys:
    an optional map with following keys:
    * :original-name - Case sensitive property name. Defaults to deriving this from db-ident
    * :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] (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))
    (assert (keyword? db-ident))
    (let [db-ident' (if (qualified-keyword? db-ident)
    (let [db-ident' (if (qualified-keyword? db-ident)
                      db-ident
                      db-ident
                      (db-property/create-user-property-ident-from-name (name db-ident)))
                      (db-property/create-user-property-ident-from-name (name db-ident)))
          prop-name (or original-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
      (block-with-timestamps
       (cond->
       (cond->
        {:db/ident db-ident'
        {:db/ident db-ident'
@@ -97,9 +96,8 @@
         :db/index true
         :db/index true
         :db/cardinality (if (= :many (:cardinality prop-schema))
         :db/cardinality (if (= :many (:cardinality prop-schema))
                           :db.cardinality/many
                           :db.cardinality/many
-                          :db.cardinality/one)}
-        block-order
-        (assoc :block/order block-order)
+                          :db.cardinality/one)
+        :block/order (db-order/gen-key)}
         (seq classes)
         (seq classes)
         (assoc :property/schema.classes classes)
         (assoc :property/schema.classes classes)
         (or ref-type? (contains? (conj db-property-type/ref-property-types :entity) (:type prop-schema)))
         (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]
   [original-page-name date-formatter]
   (when original-page-name
   (when original-page-name
     (let [page-name (common-util/page-name-sanity-lc 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))
 (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)))
             page-blocks)))
 
 
 (defn ^:api delete-block
 (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)
   (otree/-del node txs-state conn)
   @txs-state)
   @txs-state)
 
 
@@ -704,7 +704,7 @@
 (defn ^:api ^:large-vars/cleanup-todo delete-blocks
 (defn ^:api ^:large-vars/cleanup-todo delete-blocks
   "Delete blocks from the tree.
   "Delete blocks from the tree.
   `blocks` need to be sorted by left&parent(from top to bottom)"
   `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)]]
   [:pre [(seq blocks)]]
   (let [top-level-blocks (filter-top-level-blocks @conn 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)))
         non-consecutive? (and (> (count top-level-blocks) 1) (seq (ldb/get-non-consecutive-blocks @conn top-level-blocks)))
@@ -716,7 +716,7 @@
     (if (or
     (if (or
          (= 1 (count top-level-blocks))
          (= 1 (count top-level-blocks))
          (= start-block end-block))
          (= 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]
       (doseq [id block-ids]
         (let [node (d/entity @conn id)]
         (let [node (d/entity @conn id)]
           (otree/-del node txs-state conn))))
           (otree/-del node txs-state conn))))

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

@@ -2,11 +2,13 @@
   "Transact outliner ops"
   "Transact outliner ops"
   (:require [logseq.outliner.transaction :as outliner-tx]
   (:require [logseq.outliner.transaction :as outliner-tx]
             [logseq.outliner.core :as outliner-core]
             [logseq.outliner.core :as outliner-core]
+            [logseq.outliner.property :as outliner-property]
             [datascript.core :as d]
             [datascript.core :as d]
             [malli.core :as m]))
             [malli.core :as m]))
 
 
 (def ^:private op-schema
 (def ^:private op-schema
   [:multi {:dispatch first}
   [:multi {:dispatch first}
+   ;; blocks
    [:save-block
    [:save-block
     [:catn
     [:catn
      [:op :keyword]
      [:op :keyword]
@@ -30,11 +32,73 @@
    [:indent-outdent-blocks
    [:indent-outdent-blocks
     [:catn
     [:catn
      [:op :keyword]
      [: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
 (def ^:private ops-schema
   [:schema {:registry {::id int?
   [:schema {:registry {::id int?
                        ::block map?
                        ::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?]
                        ::option [:maybe map?]
                        ::blocks [:sequential ::block]
                        ::blocks [:sequential ::block]
                        ::ids [:sequential ::id]}}
                        ::ids [:sequential ::id]}}
@@ -44,6 +108,7 @@
 
 
 (defn apply-ops!
 (defn apply-ops!
   [repo conn ops date-formatter opts]
   [repo conn ops date-formatter opts]
+  ;; (prn :debug :outliner-ops ops)
   (assert (ops-validator ops) ops)
   (assert (ops-validator ops) ops)
   (let [opts' (assoc opts
   (let [opts' (assoc opts
                      :transact-opts {:conn conn}
                      :transact-opts {:conn conn}
@@ -53,6 +118,7 @@
      opts'
      opts'
      (doseq [[op args] ops]
      (doseq [[op args] ops]
        (case op
        (case op
+         ;; blocks
          :save-block
          :save-block
          (apply outliner-core/save-block! repo conn date-formatter args)
          (apply outliner-core/save-block! repo conn date-formatter args)
 
 
@@ -84,5 +150,46 @@
          (let [[block-ids indent? opts] args
          (let [[block-ids indent? opts] args
                blocks (keep #(d/entity @conn %) block-ids)]
                blocks (keep #(d/entity @conn %) block-ids)]
            (when (seq blocks)
            (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))
     @*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.export.common :as export-common-handler]
             [frontend.handler.property.util :as pu]
             [frontend.handler.property.util :as pu]
             [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.db-based.property :as db-property-handler]
+            [logseq.outliner.property :as outliner-property]
             [frontend.mobile.util :as mobile-util]
             [frontend.mobile.util :as mobile-util]
             [frontend.mobile.intent :as mobile-intent]
             [frontend.mobile.intent :as mobile-intent]
             [frontend.modules.outliner.tree :as tree]
             [frontend.modules.outliner.tree :as tree]
@@ -634,8 +635,7 @@
            :on-pointer-down
            :on-pointer-down
            (fn [e]
            (fn [e]
              (util/stop e)
              (util/stop e)
-             (db-property-handler/delete-property-value! repo
-                                                         block
+             (db-property-handler/delete-property-value! (:db/id block)
                                                          :block/tags
                                                          :block/tags
                                                          (:db/id page-entity)))}
                                                          (:db/id page-entity)))}
           (ui/icon "x" {:size 15})]))]))
           (ui/icon "x" {:size 15})]))]))
@@ -2267,7 +2267,7 @@
 
 
 (rum/defc block-closed-values-properties
 (rum/defc block-closed-values-properties
   [block]
   [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)
     (when (seq closed-values-properties)
       [:div.closed-values-properties.flex.flex-row.items-center.gap-1.select-none.h-full
       [:div.closed-values-properties.flex.flex-row.items-center.gap-1.select-none.h-full
        (for [pid closed-values-properties]
        (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.components.property.value :as pv]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.db :as db]
-            [frontend.handler.db-based.property :as db-property-handler]
+            [logseq.outliner.property :as outliner-property]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
             [frontend.state :as state]
             [frontend.state :as state]
             [rum.core :as rum]
             [rum.core :as rum]
@@ -26,7 +26,7 @@
         edit-input-id-prefix (str "edit-block-" (:block/uuid page))
         edit-input-id-prefix (str "edit-block-" (:block/uuid page))
         configure-opts {:selected? false
         configure-opts {:selected? false
                         :page-configure? configure?}
                         :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))
         has-class-properties? (seq (:class/schema.properties page))
         hide-properties? (:logseq.property/hide-properties? page)]
         hide-properties? (:logseq.property/hide-properties? page)]
     (when (or configure?
     (when (or configure?
@@ -94,7 +94,7 @@
 (rum/defc page-properties-react < rum/reactive
 (rum/defc page-properties-react < rum/reactive
   [page* page-opts]
   [page* page-opts]
   (let [page (db/sub-block (:db/id page*))]
   (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
               ;; Allow class and property pages to add new property
               (some #{"class" "property"} (:block/type page)))
               (some #{"class" "property"} (:block/type page)))
       (page-properties page page-opts))))
       (page-properties page page-opts))))

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

@@ -356,11 +356,9 @@
                 (icon-component/icon-picker icon
                 (icon-component/icon-picker icon
                                             {:on-chosen (fn [_e icon]
                                             {:on-chosen (fn [_e icon]
                                                           (db-property-handler/set-block-property!
                                                           (db-property-handler/set-block-property!
-                                                           repo
                                                            (:db/id page)
                                                            (:db/id page)
                                                            (pu/get-pid :logseq.property/icon)
                                                            (pu/get-pid :logseq.property/icon)
-                                                           icon
-                                                           {}))
+                                                           icon))
                                              :icon-props {:size 38}})
                                              :icon-props {:size 38}})
                 icon)])
                 icon)])
            [:h1.page-title.flex-1.cursor-pointer.gap-1
            [: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.async :as db-async]
             [frontend.db-mixins :as db-mixins]
             [frontend.db-mixins :as db-mixins]
             [frontend.db.model :as model]
             [frontend.db.model :as model]
+            [logseq.outliner.property :as outliner-property]
             [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.notification :as notification]
             [frontend.handler.notification :as notification]
             [frontend.handler.property :as property-handler]
             [frontend.handler.property :as property-handler]
@@ -110,11 +111,11 @@
             ;; Only ask for confirmation on class schema properties
             ;; Only ask for confirmation on class schema properties
               (js/confirm "Are you sure you want to delete this property?"))
               (js/confirm "Are you sure you want to delete this property?"))
       (let [repo (state/get-current-repo)
       (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)]
             property-id (:db/ident property)]
-        (f repo (:block/uuid block) property-id)))))
+        (f repo id property-id)))))
 
 
 (rum/defc schema-type <
 (rum/defc schema-type <
   shortcut/disable-all-shortcuts
   shortcut/disable-all-shortcuts
@@ -241,7 +242,6 @@
               (icon-component/icon-picker icon-value
               (icon-component/icon-picker icon-value
                                           {:on-chosen (fn [_e icon]
                                           {:on-chosen (fn [_e icon]
                                                         (db-property-handler/upsert-property!
                                                         (db-property-handler/upsert-property!
-                                                         (state/get-current-repo)
                                                          (:db/ident property)
                                                          (:db/ident property)
                                                          (:block/schema property)
                                                          (:block/schema property)
                                                          {:properties {:logseq.property/icon icon}}))})
                                                          {:properties {:logseq.property/icon icon}}))})
@@ -249,7 +249,6 @@
               (when icon-value
               (when icon-value
                 [:a.fade-link.flex {:on-click (fn [_e]
                 [:a.fade-link.flex {:on-click (fn [_e]
                                                 (db-property-handler/remove-block-property!
                                                 (db-property-handler/remove-block-property!
-                                                 (state/get-current-repo)
                                                  (:db/ident property)
                                                  (:db/ident property)
                                                  :logseq.property/icon))
                                                  :logseq.property/icon))
                                     :title "Delete this icon"}
                                     :title "Delete this icon"}
@@ -405,7 +404,7 @@
         (if (and (contains? (:block/type entity) "class") page-configure?)
         (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?})
           (pv/<add-property! entity property-uuid-or-name "" {:class-schema? class-schema? :exit-edit? page-configure?})
           (p/do!
           (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))
            true))
         (do (notification/show! "This is an invalid property name. A property name cannot start with page reference characters '#' or '[['." :error)
         (do (notification/show! "This is an invalid property name. A property name cannot start with page reference characters '#' or '[['." :error)
             (pv/exit-edit-property))))))
             (pv/exit-edit-property))))))
@@ -518,7 +517,7 @@
            new-property?
            new-property?
            (property-input block *property-key *property-value opts)
            (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))
                     (:page-configure? opts))
                 (not config/publishing?)
                 (not config/publishing?)
                 (not (:in-block-container? opts)))
                 (not (:in-block-container? opts)))
@@ -546,7 +545,6 @@
   (rum/local false ::hover?)
   (rum/local false ::hover?)
   [state block property {:keys [class-schema? block? collapsed? page-cp inline-text]}]
   [state block property {:keys [class-schema? block? collapsed? page-cp inline-text]}]
   (let [*hover? (::hover? state)
   (let [*hover? (::hover? state)
-        repo (state/get-current-repo)
         icon (:logseq.property/icon property)
         icon (:logseq.property/icon property)
         property-name (:block/original-name property)]
         property-name (:block/original-name property)]
     [:div.flex.flex-row.items-center
     [:div.flex.flex-row.items-center
@@ -574,7 +572,7 @@
        [:a.block-control
        [:a.block-control
         {:on-click (fn [event]
         {:on-click (fn [event]
                      (util/stop 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
         [:span {:class (cond
                          (or collapsed? @*hover?)
                          (or collapsed? @*hover?)
                          "control-show cursor-pointer"
                          "control-show cursor-pointer"
@@ -588,8 +586,7 @@
                          {:on-chosen
                          {:on-chosen
                           (fn [_e icon]
                           (fn [_e icon]
                             (when 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)
                                                                               (:block/schema property)
                                                                               {:properties {:logseq.property/icon icon}})]
                                                                               {:properties {:logseq.property/icon icon}})]
                                 (shui/popup-hide! id))))}))]
                                 (shui/popup-hide! id))))}))]
@@ -724,7 +721,7 @@
 (defn- async-load-classes!
 (defn- async-load-classes!
   [block]
   [block]
   (let [repo (state/get-current-repo)
   (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]
     (doseq [class classes]
       (db-async/<get-block repo (:db/id class) :children? false))
       (db-async/<get-block repo (:db/id class) :children? false))
     classes))
     classes))
@@ -762,7 +759,7 @@
                                                     (and (not (get-in ent [:block/schema :public?]))
                                                     (and (not (get-in ent [:block/schema :public?]))
                                                          (ldb/built-in? ent))))))
                                                          (ldb/built-in? ent))))))
                                              properties))
                                              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))
         one-class? (= 1 (count classes))
         block-own-properties (->> (concat (when (seq (:block/alias block))
         block-own-properties (->> (concat (when (seq (:block/alias block))
                                             [[:block/alias (: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.db.frontend.order :as db-order]
             [logseq.outliner.core :as outliner-core]))
             [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!
 (defn- <upsert-closed-value!
   "Create new closed value and returns its block UUID."
   "Create new closed value and returns its block UUID."
   [property item]
   [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!
     (p/do!
      (when (seq tx-data) (db/transact! (state/get-current-repo) tx-data {:outliner-op :upsert-closed-value}))
      (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)))
      block-id)))
 
 
 (rum/defc item-value
 (rum/defc item-value
@@ -110,11 +116,10 @@
 
 
 (rum/defcs choice-with-close <
 (rum/defcs choice-with-close <
   (rum/local false ::hover?)
   (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)
   (let [*hover? (::hover? state)
         value (db-property/closed-value-name item)
         value (db-property/closed-value-name item)
         page? (:block/original-name item)
         page? (:block/original-name item)
-        date? (= :date (:type (:block/schema property)))
         property-block? (db-property/property-created-block? item)]
         property-block? (db-property/property-created-block? item)]
     [:div.flex.flex-1.flex-row.items-center.gap-2.justify-between
     [:div.flex.flex-1.flex-row.items-center.gap-2.justify-between
      {:on-mouse-over #(reset! *hover? true)
      {:on-mouse-over #(reset! *hover? true)
@@ -128,14 +133,6 @@
         [:a {:on-click toggle-fn}
         [:a {:on-click toggle-fn}
          value]
          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))
         (and page? (:page-cp parent-opts))
         ((:page-cp parent-opts) {:preview? false} item)
         ((:page-cp parent-opts) {:preview? false} item)
 
 
@@ -169,12 +166,13 @@
           opts {:toggle-fn #(shui/popup-show! % content-fn)}]
           opts {:toggle-fn #(shui/popup-show! % content-fn)}]
 
 
       (choice-with-close
       (choice-with-close
-       property
        block
        block
        (assoc opts
        (assoc opts
               :delete-choice
               :delete-choice
               (fn []
               (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
               :update-icon
               (fn [icon]
               (fn [icon]
                 (property-handler/set-block-property! (state/get-current-repo) (:block/uuid block) :logseq.property/icon icon)))
                 (property-handler/set-block-property! (state/get-current-repo) (:block/uuid block) :logseq.property/icon icon)))
@@ -195,7 +193,7 @@
    (ui/button
    (ui/button
     "Add choices"
     "Add choices"
     {:on-click (fn []
     {: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)))})])
                    (toggle-fn)))})])
 
 
 (rum/defc choices < rum/reactive
 (rum/defc choices < rum/reactive
@@ -243,14 +241,14 @@
                    existing-values (seq (:property/closed-values property))
                    existing-values (seq (:property/closed-values property))
                    values (if (seq existing-values)
                    values (if (seq existing-values)
                             (let [existing-ids (set (map :db/id 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)]
                             values)]
              (shui/popup-show! (.-target e)
              (shui/popup-show! (.-target e)
                                (fn [{:keys [id]}]
                                (fn [{:keys [id]}]
                                  (let [opts {:toggle-fn (fn [] (shui/popup-hide! 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]))
                                        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?)
                                                     (remove string/blank?)
                                                     distinct)]
                                                     distinct)]
                                    (if (seq values')
                                    (if (seq values')

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

@@ -1,14 +1,12 @@
 (ns frontend.components.property.util
 (ns frontend.components.property.util
   "Property component utils"
   "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!
 (defn update-property!
   [property property-name property-schema]
   [property property-name property-schema]
   (when (or (not= (:block/original-name property) property-name)
   (when (or (not= (:block/original-name property) property-name)
             (not= (:block/schema property) property-schema))
             (not= (:block/schema property) property-schema))
     (db-property-handler/upsert-property!
     (db-property-handler/upsert-property!
-     (state/get-current-repo)
      (:db/ident property)
      (:db/ident property)
      property-schema
      property-schema
      {:property-name property-name})))
      {: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.editor :as editor-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.page :as page-handler]
             [frontend.handler.property :as property-handler]
             [frontend.handler.property :as property-handler]
+            [logseq.outliner.property :as outliner-property]
             [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.db-based.property :as db-property-handler]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.ui :as ui]
@@ -19,7 +20,6 @@
             [rum.core :as rum]
             [rum.core :as rum]
             [frontend.handler.route :as route-handler]
             [frontend.handler.route :as route-handler]
             [promesa.core :as p]
             [promesa.core :as p]
-            [goog.dom :as gdom]
             [frontend.db.async :as db-async]
             [frontend.db.async :as db-async]
             [logseq.common.util.macro :as macro-util]
             [logseq.common.util.macro :as macro-util]
             [logseq.db :as ldb]
             [logseq.db :as ldb]
@@ -40,15 +40,12 @@
                                  {:disabled? config/publishing?
                                  {:disabled? config/publishing?
                                   :on-chosen (fn [_e icon]
                                   :on-chosen (fn [_e icon]
                                                (db-property-handler/set-block-property!
                                                (db-property-handler/set-block-property!
-                                                (state/get-current-repo)
                                                 (:db/id block)
                                                 (:db/id block)
                                                 :logseq.property/icon
                                                 :logseq.property/icon
-                                                icon
-                                                {}))})
+                                                icon))})
      (when (and icon-value (not config/publishing?))
      (when (and icon-value (not config/publishing?))
        [:a.fade-link.flex {:on-click (fn [_e]
        [:a.fade-link.flex {:on-click (fn [_e]
                                        (db-property-handler/remove-block-property!
                                        (db-property-handler/remove-block-property!
-                                        (state/get-current-repo)
                                         (:db/id block)
                                         (:db/id block)
                                         :logseq.property/icon))
                                         :logseq.property/icon))
                            :title "Delete this icon"}
                            :title "Delete this icon"}
@@ -71,12 +68,15 @@
 (defn <create-new-block!
 (defn <create-new-block!
   [block property value & {:keys [edit-block?]
   [block property value & {:keys [edit-block?]
                            :or {edit-block? true}}]
                            :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!
     (p/do!
-     result
      (exit-edit-property)
      (exit-edit-property)
-     (let [block (db/entity [:block/uuid block-id])]
+     (let [block (db/entity [:block/uuid new-block-id])]
        (when edit-block?
        (when edit-block?
          (editor-handler/edit-block! block :max {:container-id :unknown-container}))
          (editor-handler/edit-block! block :max {:container-id :unknown-container}))
        block))))
        block))))
@@ -89,28 +89,29 @@
                                        :or {exit-edit? true}}]
                                        :or {exit-edit? true}}]
 
 
    (let [repo (state/get-current-repo)
    (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!
      (p/do!
       (when property-key
       (when property-key
         (if (and class? class-schema?)
         (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']
           (let [[property-id property-value']
                 (if (string? property-key)
                 (if (string? property-key)
                   (if-let [ent (ldb/get-case-page (db/get-db repo) property-key)]
                   (if-let [ent (ldb/get-case-page (db/get-db repo) property-key)]
                     [(:db/ident ent) property-value]
                     [(:db/ident ent) property-value]
                     ;; This is a new property. Create a new property id to use of set-block-property!
                     ;; 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/get-db (state/get-current-repo))
                       (db-property/create-user-property-ident-from-name property-key))
                       (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])]
                   [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?
       (when exit-edit?
         (shui/popup-hide!)
         (shui/popup-hide!)
         (exit-edit-property))))))
         (exit-edit-property))))))
@@ -377,11 +378,17 @@
 (defn <create-new-block-from-template!
 (defn <create-new-block-from-template!
   "`template`: tag block"
   "`template`: tag block"
   [block property template]
   [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
 (rum/defcs select < rum/reactive
   {:init (fn [state]
   {:init (fn [state]
@@ -411,7 +418,7 @@
                                         value)
                                         value)
                                :value (:db/id block)})) (:property/closed-values property))
                                :value (:db/id block)})) (:property/closed-values property))
                     (->> values
                     (->> values
-                         (mapcat (fn [[_id value]]
+                         (mapcat (fn [value]
                                    (if (coll? value)
                                    (if (coll? value)
                                      (map (fn [v] {:value v}) value)
                                      (map (fn [v] {:value v}) value)
                                      [{:value value}])))
                                      [{:value value}])))
@@ -686,14 +693,6 @@
         (editor-box editor-args editor-id config)])
         (editor-box editor-args editor-id config)])
      nil)])
      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
 (defn- property-value-inner
   [block property value {:keys [inline-text block-cp page-cp
   [block property value {:keys [inline-text block-cp page-cp
                                 editor-id dom-id row?
                                 editor-id dom-id row?
@@ -829,7 +828,7 @@
          [editing?])
          [editing?])
 
 
         (if (and dropdown? (not 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]}]
                 content-fn (fn [{:keys [_id content-props]}]
                              (select-cp {:content-props content-props}))]
                              (select-cp {:content-props content-props}))]
             [:div.multi-values.jtrigger
             [:div.multi-values.jtrigger

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

@@ -80,13 +80,29 @@
 (defn <get-block-property-values
 (defn <get-block-property-values
   [graph property-id]
   [graph property-id]
   (<q graph {:transact-db? false}
   (<q graph {:transact-db? false}
-      '[:find ?b ?v
+      '[:find [?v ...]
         :in $ ?property-id
         :in $ ?property-id
         :where
         :where
         [?b ?property-id ?v]
         [?b ?property-id ?v]
         [(not= ?v :logseq.property/empty-placeholder)]]
         [(not= ?v :logseq.property/empty-placeholder)]]
       property-id))
       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
 ;; TODO: batch queries for better performance and UX
 (defn <get-block
 (defn <get-block
   [graph name-or-uuid & {:keys [children? nested-children?]
   [graph name-or-uuid & {:keys [children? nested-children?]

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

@@ -368,7 +368,7 @@
                         (concat tx-data
                         (concat tx-data
                                 (db-fix/fix-cardinality-many->one @conn (:property-id tx-meta)))
                                 (db-fix/fix-cardinality-many->one @conn (:property-id tx-meta)))
                         tx-data)
                         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]
                         (map (fn [m]
                                (if (and (map? m) (nil? (:block/order m)))
                                (if (and (map? m) (nil? (:block/order m)))
                                  (assoc m :block/order (db-order/gen-key nil))
                                  (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
 (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]
             [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!
 (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!
 (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!
 (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!
 (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!
 (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!
 (defn batch-set-property-closed-value!
   [block-ids db-ident 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]
   (every? (fn [id]
             (:hide? (:block/schema (db/entity id)))) properties))
             (:hide? (:block/schema (db/entity id)))) properties))
 
 
-;; FIXME: property no long has `:block/name` attribute
 (defn readable-properties
 (defn readable-properties
   "Given a DB graph's properties, returns a readable properties map with keys as
   "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
   property names and property values dereferenced where possible. A property's
@@ -34,9 +33,3 @@
                   (set (map readable-property-val v))
                   (set (map readable-property-val v))
                   (readable-property-val v))])))
                   (readable-property-val v))])))
        (into {})))
        (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.config :as config]
             [frontend.date :as date]
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db :as db]
+            [frontend.db.async :as db-async]
             [frontend.db.model :as db-model]
             [frontend.db.model :as db-model]
+            [frontend.db.query-dsl :as query-dsl]
             [frontend.db.utils :as db-utils]
             [frontend.db.utils :as db-utils]
-            [frontend.db.async :as db-async]
             [frontend.diff :as diff]
             [frontend.diff :as diff]
+            [frontend.extensions.pdf.utils :as pdf-utils]
             [frontend.format.block :as block]
             [frontend.format.block :as block]
             [frontend.format.mldoc :as mldoc]
             [frontend.format.mldoc :as mldoc]
             [frontend.fs :as fs]
             [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.assets :as assets-handler]
             [frontend.handler.block :as block-handler]
             [frontend.handler.block :as block-handler]
             [frontend.handler.common :as common-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.db-based.property.util :as db-pu]
             [frontend.handler.export.html :as export-html]
             [frontend.handler.export.html :as export-html]
             [frontend.handler.export.text :as export-text]
             [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.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.repeated :as repeated]
             [frontend.handler.route :as route-handler]
             [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]
             [frontend.mobile.util :as mobile-util]
-            [logseq.outliner.core :as outliner-core]
             [frontend.modules.outliner.op :as outliner-op]
             [frontend.modules.outliner.op :as outliner-op]
-            [frontend.modules.outliner.ui :as ui-outliner-tx]
             [frontend.modules.outliner.tree :as tree]
             [frontend.modules.outliner.tree :as tree]
+            [frontend.modules.outliner.ui :as ui-outliner-tx]
             [frontend.search :as search]
             [frontend.search :as search]
             [frontend.state :as state]
             [frontend.state :as state]
             [frontend.template :as template]
             [frontend.template :as template]
@@ -43,30 +45,28 @@
             [frontend.util.drawer :as drawer]
             [frontend.util.drawer :as drawer]
             [frontend.util.keycode :as keycode]
             [frontend.util.keycode :as keycode]
             [frontend.util.list :as list]
             [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.text :as text-util]
             [frontend.util.thingatpt :as thingatpt]
             [frontend.util.thingatpt :as thingatpt]
+            [goog.crypt.base64 :as base64]
             [goog.dom :as gdom]
             [goog.dom :as gdom]
             [goog.dom.classes :as gdom-classes]
             [goog.dom.classes :as gdom-classes]
             [goog.object :as gobj]
             [goog.object :as gobj]
-            [goog.crypt.base64 :as base64]
             [goog.string :as gstring]
             [goog.string :as gstring]
             [lambdaisland.glogi :as log]
             [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.db.frontend.schema :as db-schema]
             [logseq.graph-parser.block :as gp-block]
             [logseq.graph-parser.block :as gp-block]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.text :as text]
             [logseq.graph-parser.text :as text]
             [logseq.graph-parser.utf8 :as utf8]
             [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]
             [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
 ;; 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]
   [repo block-id property-id-or-key]
   (if (config/db-based-graph? repo)
   (if (config/db-based-graph? repo)
     (let [eid (if (uuid? block-id) [:block/uuid block-id] block-id)]
     (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)))
     (file-property-handler/remove-block-property! block-id property-id-or-key)))
 
 
 (defn set-block-property!
 (defn set-block-property!
-  [repo block-id key v & opts]
+  [repo block-id key v]
   (if (config/db-based-graph? repo)
   (if (config/db-based-graph? repo)
     (let [eid (if (uuid? block-id) [:block/uuid block-id] block-id)]
     (let [eid (if (uuid? block-id) [:block/uuid block-id] block-id)]
       (if (or (nil? v) (and (coll? v) (empty? v)))
       (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)))
     (file-property-handler/set-block-property! block-id key v)))
 
 
 (defn add-page-property!
 (defn add-page-property!
@@ -56,13 +56,13 @@
 (defn batch-remove-block-property!
 (defn batch-remove-block-property!
   [repo block-ids key]
   [repo block-ids key]
   (if (config/db-based-graph? repo)
   (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)))
     (file-property-handler/batch-remove-block-property! block-ids key)))
 
 
 (defn batch-set-block-property!
 (defn batch-set-block-property!
   [repo block-ids key value]
   [repo block-ids key value]
   (if (config/db-based-graph? repo)
   (if (config/db-based-graph? repo)
     (if (nil? value)
     (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)))
     (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)
   (let [repo (state/get-current-repo)
         db (db/get-db repo)]
         db (db/get-db repo)]
     (db-property/get-closed-property-values db property-id)))
     (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)
   nil)
 
 
 (defn- op-transact!
 (defn- op-transact!
-  [fn-var & args]
-  {:pre [(var? fn-var)]}
+  [result]
   (when (nil? *outliner-ops*)
   (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!
 (defn save-block!
   [block & {:as opts}]
   [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!
 (defn insert-blocks!
   [blocks target-block opts]
   [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!
 (defn delete-blocks!
   [blocks opts]
   [blocks opts]
-  (op-transact! #'delete-blocks blocks opts))
+  (op-transact!
+   (let [ids (map :db/id blocks)]
+    [:delete-blocks [ids opts]])))
 
 
 (defn move-blocks!
 (defn move-blocks!
   [blocks target-block sibling?]
   [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!
 (defn move-blocks-up-down!
   [blocks up?]
   [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!
 (defn indent-outdent-blocks!
   [blocks indent? & {:as opts}]
   [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 :as db]
             [frontend.db.model :as db-model]
             [frontend.db.model :as db-model]
             [frontend.db.utils :as db-utils]
             [frontend.db.utils :as db-utils]
-            [logseq.db :as ldb]))
+            [logseq.db :as ldb]
+            [datascript.core :as d]))
 
 
 (def fuzzy-search fuzzy/fuzzy-search)
 (def fuzzy-search fuzzy/fuzzy-search)
 
 
@@ -137,7 +138,7 @@
   (when-let [repo (state/get-current-repo)]
   (when-let [repo (state/get-current-repo)]
     (p/let [page (db/entity page-id)
     (p/let [page (db/entity page-id)
             alias-names (conj (set (map util/safe-page-name-sanity-lc
             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))
                               (:block/original-name page))
             q (string/join " " alias-names)
             q (string/join " " alias-names)
             result (block-search repo q {:limit 100})
             result (block-search repo q {:limit 100})
@@ -145,7 +146,7 @@
             result (when (seq eids)
             result (when (seq eids)
                      (.get-page-unlinked-refs ^Object @state/*db-worker repo (:db/id page) (ldb/write-transit-str 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))]
             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'
       (some->> result'
                db-model/sort-by-order-recursive
                db-model/sort-by-order-recursive
                db-utils/group-by-page))))
                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/window.msRequestAnimationFrame))
          #(js/setTimeout % 16))))
          #(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
 #?(:cljs
    (defn parse-params
    (defn parse-params
      "Parse URL parameters in hash(fragment) into a hashmap"
      "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.db :as db]
             [frontend.test.helper :as test-helper]
             [frontend.test.helper :as test-helper]
             [datascript.core :as d]
             [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]
             [frontend.handler.page :as page-handler]
             [logseq.db :as ldb]))
             [logseq.db :as ldb]))
 
 
@@ -23,15 +23,20 @@
 (use-fixtures :each start-and-destroy-db)
 (use-fixtures :each start-and-destroy-db)
 
 
 (deftest get-block-property-values-test
 (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
 (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
 ;; (deftest get-db-property-values-test-with-pages
 ;;   (let [opts {:redirect? false :create-first-block? false}
 ;;   (let [opts {:redirect? false :create-first-block? false}
@@ -39,9 +44,9 @@
 ;;         _ (page-handler/create! "page2" opts)
 ;;         _ (page-handler/create! "page2" opts)
 ;;         p1id (:block/uuid (db/get-page "page1"))
 ;;         p1id (:block/uuid (db/get-page "page1"))
 ;;         p2id (:block/uuid (db/get-page "page2"))]
 ;;         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")))))
 ;;     (is (= '("[[page1]]" "[[page2]]") (model/get-db-property-values repo "property-1")))))
 
 
 (deftest get-all-classes-test
 (deftest get-all-classes-test
@@ -74,10 +79,11 @@
         _ (page-handler/create! "class1" opts)
         _ (page-handler/create! "class1" opts)
         _ (page-handler/create! "class2" opts)
         _ (page-handler/create! "class2" opts)
         class1 (db/get-page "class1")
         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)
     (let [property (db/entity :user.property/property-1)
           classes (model/get-classes-with-property (:db/ident property))]
           classes (model/get-classes-with-property (:db/ident property))]
       (is (= (set (map :db/id classes))
       (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
 (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]
             [frontend.db :as db]
             [clojure.test :refer [deftest is testing are use-fixtures]]
             [clojure.test :refer [deftest is testing are use-fixtures]]
             [frontend.test.helper :as test-helper]
             [frontend.test.helper :as test-helper]
@@ -34,7 +34,10 @@
 ;; update-property!
 ;; update-property!
 (deftest ^:large-vars/cleanup-todo block-property-test
 (deftest ^:large-vars/cleanup-todo block-property-test
   (testing "Add a property to a block"
   (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])
     (let [block (db/entity [:block/uuid fbid])
           properties (:block/properties block)
           properties (:block/properties block)
           property (db/entity :user.property/property-1)]
           property (db/entity :user.property/property-1)]
@@ -54,7 +57,11 @@
         "value")))
         "value")))
 
 
   (testing "Add another property"
   (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])
     (let [block (db/entity [:block/uuid fbid])
           properties (:block/properties block)
           properties (:block/properties block)
           property (db/entity :user.property/property-2)]
           property (db/entity :user.property/property-2)]
@@ -70,54 +77,58 @@
         2
         2
         (every? keyword? (map first properties))
         (every? keyword? (map first properties))
         true
         true
-        (second (second properties))
-        1)))
+        (:block/content (second (second properties)))
+        "1")))
 
 
   (testing "Update property value"
   (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])
     (let [block (db/entity [:block/uuid fbid])
           properties (:block/properties block)]
           properties (:block/properties block)]
       ;; check block's properties
       ;; check block's properties
       (are [x y] (= x y)
       (are [x y] (= x y)
         (count properties)
         (count properties)
         2
         2
-        (second (second properties))
-        2)))
+        (:block/content (second (second properties)))
+        "2")))
 
 
   (testing "Wrong type property value shouldn't transacted"
   (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])
     (let [block (db/entity [:block/uuid fbid])
           properties (:block/properties block)]
           properties (:block/properties block)]
       ;; check block's properties
       ;; check block's properties
       (are [x y] (= x y)
       (are [x y] (= x y)
         (count properties)
         (count properties)
         2
         2
-        (second (second properties))
-        2)))
+        (:block/content (second (second properties)))
+        "2")))
 
 
   (testing "Add a multi-values property"
   (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
       ;; 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
       ;; 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"
   (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])
     (let [block (db/entity [:block/uuid fbid])
           properties (:block/properties block)]
           properties (:block/properties block)]
       ;; check block's properties
       ;; check block's properties
@@ -129,9 +140,10 @@
 
 
   (testing "Batch set properties"
   (testing "Batch set properties"
     (let [k :user.property/property-4
     (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])
       (let [fb (db/entity [:block/uuid fbid])
             sb (db/entity [:block/uuid sbid])]
             sb (db/entity [:block/uuid sbid])]
         (are [x y] (= x y)
         (are [x y] (= x y)
@@ -142,7 +154,7 @@
 
 
   (testing "Batch remove properties"
   (testing "Batch remove properties"
     (let [k :user.property/property-4]
     (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])
       (let [fb (db/entity [:block/uuid fbid])
             sb (db/entity [:block/uuid sbid])]
             sb (db/entity [:block/uuid sbid])]
         (are [x y] (= x y)
         (are [x y] (= x y)
@@ -162,8 +174,8 @@
         _ (page-handler/create! "class3" opts)
         _ (page-handler/create! "class3" opts)
         c1 (db/get-page "class1")
         c1 (db/get-page "class1")
         c2 (db/get-page "class2")
         c2 (db/get-page "class2")
-        c1id (:block/uuid c1)
-        c2id (:block/uuid c2)]
+        c1id (:db/id c1)
+        c2id (:db/id c2)]
 
 
     (testing "Create classes"
     (testing "Create classes"
       (are [x y] (= x y)
       (are [x y] (= x y)
@@ -173,16 +185,18 @@
         #{"class"}))
         #{"class"}))
 
 
     (testing "Class add property"
     (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
       ;; 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
       ;; 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"
     (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)))))))
       (is (= 2 (count (:class/schema.properties (db/entity (:db/id c1)))))))
     (testing "Add classes to a block"
     (testing "Add classes to a block"
       (test-helper/save-block! repo fbid "Block 1" {:tags ["class1" "class2" "class3"]})
       (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]))))))
         (is (= 2 (count (:block/tags (db/entity [:block/uuid fbid]))))))
     (testing "Get block's classes properties"
     (testing "Get block's classes properties"
       ;; set c2 as parent of c3
       ;; 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)
         (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
       (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
 ;; convert-property-input-string
@@ -210,7 +225,7 @@
     (let [test-uuid (random-uuid)]
     (let [test-uuid (random-uuid)]
       (are [x y]
       (are [x y]
            (= (let [[schema-type value] x]
            (= (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"] 1
         [:number "1.2"] 1.2
         [:number "1.2"] 1.2
         [:url test-uuid] test-uuid
         [:url test-uuid] test-uuid
@@ -220,15 +235,17 @@
 
 
 (deftest upsert-property!
 (deftest upsert-property!
   (testing "Update an existing 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)))))
       (is (db-property/many? (db/entity repo :user.property/p0)))))
   (testing "Multiple properties that generate the same initial :db/ident"
   (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}}
       (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]))
              (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)
 ;; template (TBD, template implementation not settle down yet)
 ;; property-create-new-block-from-template
 ;; 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)
 #_(cljs.test/run-tests)