property.cljs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. (ns frontend.handler.property
  2. "Block properties handler."
  3. (:require [clojure.edn :as edn]
  4. [clojure.string :as string]
  5. [frontend.db :as db]
  6. [frontend.db.model :as model]
  7. [frontend.handler.notification :as notification]
  8. [frontend.modules.outliner.core :as outliner-core]
  9. [frontend.state :as state]
  10. [frontend.util :as util]
  11. [logseq.graph-parser.util :as gp-util]
  12. [malli.util :as mu]
  13. [malli.error :as me]))
  14. ;; TODO:
  15. ;; Validate && list fixes for non-validated values when updating property schema
  16. (defn- date-str?
  17. [value]
  18. (when-let [d (js/Date. value)]
  19. (not= (str d) "Invalid Date")))
  20. (defn- logseq-page?
  21. [id]
  22. (and (uuid? id)
  23. (when-let [e (db/entity [:block/uuid id])]
  24. (nil? (:block/page e)))))
  25. (defn- logseq-block?
  26. [id]
  27. (and (uuid? id)
  28. (when-let [e (db/entity [:block/uuid id])]
  29. (some? (:block/page e)))))
  30. (defn- logseq-object?
  31. [id]
  32. (and (uuid? id)
  33. (when-let [e (db/entity [:block/uuid id])]
  34. (seq (:block/class e)))))
  35. (def builtin-schema-types
  36. {:default string? ; refs/tags will not be extracted
  37. :number number?
  38. :date [:fn
  39. {:error/message "should be a date"}
  40. date-str?]
  41. :checkbox boolean?
  42. :url [:fn
  43. {:error/message "should be a URL"}
  44. gp-util/url?]
  45. :page [:fn
  46. {:error/message "should be a page"}
  47. logseq-page?]
  48. :block [:fn
  49. {:error/message "should be a block"}
  50. logseq-block?]
  51. :object [:fn
  52. {:error/message "should be an object"}
  53. logseq-object?]})
  54. ;; schema -> type, cardinality, object's class
  55. ;; min, max -> string length, number range, cardinality size limit
  56. ;; TODO: Enable or delete if unused
  57. #_(def builtin-schema->type
  58. (set/map-invert builtin-schema-types))
  59. (defn- infer-schema-from-input-string
  60. [v-str]
  61. (try
  62. (cond
  63. (parse-long v-str) :number
  64. (parse-double v-str) :number
  65. (util/uuid-string? v-str) :object
  66. (gp-util/url? v-str) :url
  67. (date-str? v-str) :date
  68. (contains? #{"true" "false"} (string/lower-case v-str)) :boolean
  69. :else :default)
  70. (catch :default _e
  71. :default)))
  72. (defn convert-property-input-string
  73. [schema-type v-str]
  74. (if (and (not (string? v-str)) (not (object? v-str)))
  75. v-str
  76. (case schema-type
  77. :default
  78. v-str
  79. :number
  80. (edn/read-string v-str)
  81. :boolean
  82. (edn/read-string (string/lower-case v-str))
  83. :page
  84. (uuid v-str)
  85. :block
  86. (uuid v-str)
  87. :object
  88. (uuid v-str)
  89. :date
  90. (js/Date. v-str) ; inst
  91. :url
  92. v-str)))
  93. (defn- upsert-property!
  94. [repo property k-name property-uuid property-type]
  95. (let [k-name (name k-name)]
  96. (when (and property (nil? (:block/type property)))
  97. (db/transact! repo [(outliner-core/block-with-updated-at
  98. {:block/schema {:type property-type}
  99. :block/uuid property-uuid
  100. :block/type "property"})]
  101. {:outliner-op :update-property}))
  102. (when (nil? property) ;if property not exists yet
  103. (db/transact! repo [(outliner-core/block-with-timestamps
  104. {:block/schema {:type property-type}
  105. :block/original-name k-name
  106. :block/name (util/page-name-sanity-lc k-name)
  107. :block/uuid property-uuid
  108. :block/type "property"})]
  109. {:outliner-op :create-new-property}))))
  110. (defn add-property!
  111. [repo block k-name v & {:keys [old-value]}]
  112. (let [k-name (name k-name)
  113. property (db/pull repo '[*] [:block/name (gp-util/page-name-sanity-lc k-name)])
  114. v (if property v (or v ""))]
  115. (when (some? v)
  116. (let [property-uuid (or (:block/uuid property) (random-uuid))
  117. {:keys [type cardinality]} (:block/schema property)
  118. multiple-values? (= cardinality :many)
  119. infer-schema (when-not type (infer-schema-from-input-string v))
  120. property-type (or type infer-schema :default)
  121. schema (get builtin-schema-types property-type)
  122. properties (:block/properties block)
  123. value (get properties property-uuid)
  124. v* (try
  125. (convert-property-input-string property-type v)
  126. (catch :default e
  127. (notification/show! (str e) :error false)
  128. nil))]
  129. (when-not (contains? (if (set? value) value #{value}) v*)
  130. (if-let [msg (me/humanize (mu/explain-data schema v*))]
  131. (let [msg' (str "\"" k-name "\"" " " (if (coll? msg) (first msg) msg))]
  132. (notification/show! msg' :warning))
  133. (do
  134. (upsert-property! repo property k-name property-uuid property-type)
  135. (let [new-value (cond
  136. (and multiple-values? old-value
  137. (not= old-value :frontend.components.property/new-value-placeholder))
  138. (if (coll? v*)
  139. (vec (distinct (concat value v*)))
  140. (let [v (mapv (fn [x] (if (= x old-value) v* x)) value)]
  141. (if (contains? (set v) v*)
  142. v
  143. (conj v v*))))
  144. multiple-values?
  145. (let [f (if (coll? v*) concat conj)]
  146. (vec (distinct (f value v*))))
  147. :else
  148. v*)
  149. new-value (if (coll? new-value)
  150. (vec (remove string/blank? new-value))
  151. new-value)
  152. block-properties (assoc properties property-uuid new-value)
  153. refs (outliner-core/rebuild-block-refs block block-properties)]
  154. ;; TODO: fix block/properties-order
  155. (db/transact! repo
  156. [[:db/retract (:db/id block) :block/refs]
  157. {:block/uuid (:block/uuid block)
  158. :block/properties block-properties
  159. :block/refs refs}]
  160. {:outliner-op :add-property})))))))))
  161. (defn remove-property!
  162. [repo block property-uuid]
  163. {:pre (string? property-uuid)}
  164. (let [origin-properties (:block/properties block)]
  165. (when (contains? (set (keys origin-properties)) property-uuid)
  166. (let [properties' (dissoc origin-properties property-uuid)
  167. refs (outliner-core/rebuild-block-refs block properties')]
  168. (db/transact!
  169. repo
  170. [[:db/retract (:db/id block) :block/refs]
  171. {:block/uuid (:block/uuid block)
  172. :block/properties properties'
  173. :block/refs refs}]
  174. {:outliner-op :remove-property})))))
  175. (defn- fix-cardinality-many-values!
  176. [repo property-uuid]
  177. (let [ev (->> (model/get-block-property-values property-uuid)
  178. (remove (fn [[_ v]] (coll? v))))
  179. tx-data (map (fn [[e v]]
  180. (let [entity (db/entity e)
  181. properties (:block/properties entity)]
  182. {:db/id e
  183. :block/properties (assoc properties property-uuid [v])})) ev)]
  184. (when (seq tx-data)
  185. (db/transact! repo tx-data
  186. {:outliner-op :property-fix-cardinality}))))
  187. (defn update-property!
  188. [repo property-uuid {:keys [property-name property-schema]}]
  189. {:pre [(uuid? property-uuid)]}
  190. (when-let [property (db/entity [:block/uuid property-uuid])]
  191. (when (and (= :many (:cardinality property-schema))
  192. (not= :many (:cardinality (:block/schema property))))
  193. ;; cardinality changed from :one to :many
  194. (fix-cardinality-many-values! repo property-uuid))
  195. (let [tx-data (cond-> {:block/uuid property-uuid}
  196. property-name (merge
  197. {:block/original-name property-name
  198. :block/name (gp-util/page-name-sanity-lc property-name)})
  199. property-schema (assoc :block/schema property-schema)
  200. true outliner-core/block-with-updated-at)]
  201. (db/transact! repo [tx-data]
  202. {:outliner-op :update-property}))))
  203. (defn delete-property-value!
  204. "Delete value if a property has multiple values"
  205. [repo block property-id property-value]
  206. (when (and block (uuid? property-id))
  207. (when (not= property-id (:block/uuid block))
  208. (when-let [property (db/pull [:block/uuid property-id])]
  209. (let [schema (:block/schema property)]
  210. (when (= :many (:cardinality schema))
  211. (let [properties (:block/properties block)
  212. properties' (update properties property-id
  213. (fn [col]
  214. (vec (remove #{property-value} col))))
  215. refs (outliner-core/rebuild-block-refs block properties')]
  216. (db/transact! repo
  217. [[:db/retract (:db/id block) :block/refs]
  218. {:block/uuid (:block/uuid block)
  219. :block/properties properties'
  220. :block/refs refs}]
  221. {:outliner-op :delete-property-value})))
  222. (state/clear-edit!))))))
  223. (defn set-editing-new-property!
  224. [value]
  225. (state/set-state! :ui/new-property-input-id value))
  226. (defn editing-new-property!
  227. []
  228. (set-editing-new-property! (state/get-edit-input-id))
  229. (state/clear-edit!))
  230. (defn class-add-property!
  231. [repo class k-name]
  232. (when (= "class" (:block/type class))
  233. (let [k-name (name k-name)
  234. property (db/pull repo '[*] [:block/name (gp-util/page-name-sanity-lc k-name)])
  235. property-uuid (or (:block/uuid property) (random-uuid))
  236. property-type (get-in property [:block/schema :type] :default)
  237. {:keys [properties] :as class-schema} (:block/schema class)
  238. _ (upsert-property! repo property k-name property-uuid property-type)
  239. new-properties (vec (distinct (conj properties property-uuid)))
  240. class-new-schema (assoc class-schema :properties new-properties)]
  241. (db/transact! repo
  242. [{:db/id (:db/id class)
  243. :block/schema class-new-schema}]
  244. {:outliner-op :class-add-property}))))
  245. (defn class-remove-property!
  246. [repo class k-uuid]
  247. (when (= "class" (:block/type class))
  248. (when-let [property (db/pull repo '[*] [:block/uuid k-uuid])]
  249. (let [property-uuid (:block/uuid property)
  250. {:keys [properties] :as class-schema} (:block/schema class)
  251. new-properties (vec (distinct (remove #{property-uuid} properties)))
  252. class-new-schema (assoc class-schema :properties new-properties)]
  253. (db/transact! repo [{:db/id (:db/id class)
  254. :block/schema class-new-schema}]
  255. {:outliner-op :class-remove-property})))))