| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792 |
- (ns frontend.handler.db-based.property
- "Properties handler for db graphs"
- (:require [clojure.string :as string]
- [frontend.db :as db]
- [frontend.db.model :as model]
- [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.util :as db-property-util]
- [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]
- [logseq.db.frontend.property :as db-property]
- [frontend.handler.property.util :as pu]
- [promesa.core :as p]
- [frontend.db.async :as db-async]
- [logseq.db :as ldb]))
- ;; 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 value
- (let [property-pair-e (db-property/get-pair-e block property-id)
- property-tx-data (outliner-core/block-with-updated-at
- (if property-pair-e
- {:db/id (:db/id property-pair-e)
- property-id value}
- {:db/id (:db/id block)
- :block/properties (sqlite-util/build-property-pair property-id value)}))
- block-tx-data (cond-> (outliner-core/block-with-updated-at {:db/id (:db/id block)})
- status?
- (assoc :block/tags :logseq.class/task))]
- [property-tx-data block-tx-data]))))
- (defn built-in-validation-schemas
- "A frontend version of built-in-validation-schemas that adds the current database to
- schema fns"
- [property & {:keys [new-closed-value?]
- :or {new-closed-value? false}}]
- (into {}
- (map (fn [[property-type property-val-schema]]
- (cond
- (db-property-type/closed-value-property-types property-type)
- (let [[_ schema-opts schema-fn] property-val-schema
- schema-fn' (if (db-property-type/property-types-with-db property-type) #(schema-fn (db/get-db) %) schema-fn)]
- [property-type [:fn
- schema-opts
- #((db-property-type/type-or-closed-value? schema-fn') (db/get-db) property % new-closed-value?)]])
- (db-property-type/property-types-with-db property-type)
- (let [[_ schema-opts schema-fn] property-val-schema]
- [property-type [:fn schema-opts #(schema-fn (db/get-db) %)]])
- :else
- [property-type property-val-schema]))
- db-property-type/built-in-validation-schemas)))
- (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
- (util/uuid-string? v-str) :page
- (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 (not (string? v-str)) (not (object? v-str)))
- v-str
- (case schema-type
- :number
- (fail-parse-double v-str)
- :page
- (uuid v-str)
- ;; these types don't need to be translated. :date expects uuid and other
- ;; types usually expect text
- (:url :date :any)
- v-str
- ;; :default
- (if (util/uuid-string? v-str) (uuid v-str) v-str))))
- (defn update-schema
- [property {:keys [type cardinality]}]
- (let [ident (:db/ident property)
- cardinality (if (= cardinality :many) :db.cardinality/many :db.cardinality/one)
- type-data (when (and type (db-property-type/ref-property-types type)) ; type changes
- {:db/ident ident
- :db/valueType :db.type/ref
- :db/cardinality cardinality})]
- (or type-data
- {:db/ident ident
- :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))
- (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))]
- (assert (qualified-keyword? db-ident))
- (if-let [property (and (qualified-keyword? property-id) (db/entity db-ident))]
- (let [tx-data (->>
- (concat
- [(cond->
- (outliner-core/block-with-updated-at
- {:db/ident db-ident
- :block/schema schema})
- property-name
- (assoc :block/original-name property-name))]
- (when (seq properties)
- (mapcat
- (fn [[property-id v]]
- (build-property-value-tx-data property property-id v)) properties))
- [(update-schema property schema)])
- (remove nil?))
- many->one? (and (= (:db/cardinality property) :db.cardinality/many)
- (= :one (:cardinality schema)))]
- (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})]
- {:outliner-op :new-property})))))
- (defn validate-property-value
- [schema value]
- (me/humanize (mu/explain-data schema value)))
- (defn- reset-block-property-multiple-values!
- [repo block-eid property-id values _opts]
- (let [block (db/entity repo block-eid)
- property (db/entity property-id)
- property-name (:block/original-name property)
- values (remove nil? values)
- property-schema (:block/schema property)
- {:keys [type cardinality]} property-schema
- multiple-values? (= cardinality :many)]
- (when (and multiple-values? (seq values))
- (let [infer-schema (when-not type (infer-schema-from-input-string (first values)))
- property-type (or type infer-schema :default)
- schema (get (built-in-validation-schemas property) property-type)
- values' (try
- (set (map #(convert-property-input-string property-type %) values))
- (catch :default e
- (notification/show! (str e) :error false)
- nil))
- tags-or-alias? (contains? db-property/db-attribute-properties property-id)
- old-values (if tags-or-alias?
- (->> (get block property-id)
- (map (fn [e] (:db/id e))))
- (get block (:db/ident property)))]
- (when (not= old-values values')
- (if-let [msg (some #(validate-property-value schema %) values')]
- (let [msg' (str "\"" property-name "\"" " " (if (coll? msg) (first msg) msg))]
- (notification/show! msg' :warning))
- (do
- (when-not tags-or-alias? (upsert-property! repo property-id (assoc property-schema :type property-type) {}))
- (let [pair-id (:db/id (db-property/get-pair-e block property-id))
- tx-data (concat
- [(when pair-id [:db/retract pair-id property-id])]
- (build-property-value-tx-data block property-id values' false))]
- (db/transact! repo tx-data {:outliner-op :save-block})))))))))
- (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-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))
- (defn set-block-property!
- [repo block-eid property-id v {:keys [property-name] :as opts}]
- (let [block-eid (->eid block-eid)
- _ (assert (keyword? property-id) "property-id should be a keyword")
- block (db/entity repo block-eid)
- property (db/entity property-id)
- v (if (and (uuid? v)
- (= :entity (get-in property [:block/schema :type])))
- (:db/id (db/entity [:block/uuid v]))
- v)
- k-name (:block/original-name property)
- property-schema (:block/schema property)
- {:keys [type cardinality]} property-schema
- multiple-values? (= cardinality :many)
- v (or (resolve-tag v) v)]
- (if (and multiple-values? (coll? v))
- (reset-block-property-multiple-values! repo block-eid property-id v opts)
- (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 infer-schema :default)
- schema (get (built-in-validation-schemas property) property-type)
- value (when-let [id (:db/ident property)]
- (get block id))
- v* (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)))
- tags-or-alias? (and (contains? db-property/db-attribute-properties property-id) (uuid? v*))]
- (if tags-or-alias?
- (let [property-value-id v*]
- (db/transact! repo
- [[:db/add (:db/id block) property-id property-value-id]]
- {:outliner-op :save-block}))
- (when-not (contains? (if (set? value) value #{value}) v*)
- (if-let [msg (when-not (= v* :logseq.property/empty-placeholder) (validate-property-value schema v*))]
- (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))
- value (if (= value :logseq.property/empty-placeholder) [] value)
- new-value (cond
- multiple-values?
- (let [f (if (coll? v*) concat conj)]
- (f value v*))
- :else
- v*)
- ;; 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?)]
- (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 (db/entity [:block/original-name 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 class-set-schema!
- [repo class-uuid schema]
- (when-let [class (db/entity repo [:block/uuid class-uuid])]
- (when (contains? (:block/type class) "class")
- (db/transact! repo [{:db/id (:db/id class)
- :block/schema schema}]
- {: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)
- {:keys [cardinality]} (:block/schema property)
- status? (= :logseq.task/status (:db/ident property))
- txs (->>
- (mapcat
- (fn [eid]
- (when-let [block (db/entity eid)]
- (when (and (some? v) (not= cardinality :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)
- (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? (and (= :default (get-in property [:block/schema :type] :default))
- (uuid? value))
- property-block (when block-value? (db/entity [:block/uuid value]))
- retract-blocks-tx (when (and property-block
- (some? (get property-block :logseq.property/created-from-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
- (outliner-core/->Block property-block)
- {:children? true})
- @txs-state))
- pair-id (:db/id (db-property/get-pair-e block property-id))]
- (concat
- (when pair-id
- [[:db/retractEntity pair-id]])
- retract-blocks-tx))))
- block-eids)]
- (when (seq txs)
- (db/transact! repo txs {:outliner-op :save-block}))))))
- (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))))
- (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)]
- (let [schema (:block/schema property)]
- (if (= :many (:cardinality schema))
- (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]
- (and
- (some? (get block 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 (filter db-property/property? (keys block))]
- (->> (:classes-properties (get-block-classes-properties eid))
- (concat own-properties)
- (filter (fn [id] (closed-value-other-position? id block)))
- (distinct))))
- (defn block-has-viewable-properties?
- [block-entity]
- (let [properties (->> (keys block-entity) (filter db-property/property?))]
- (or
- (seq (:block/alias block-entity))
- (and (seq properties)
- (not= properties [:logseq.property/icon])))))
- (defn property-create-new-block
- [block property value parse-block]
- (let [current-page-id (:db/id (or (:block/page block) block))
- page-name (str "$$$" current-page-id)
- page-entity (db/get-page page-name)
- page (or page-entity
- (-> (block/page-name->map page-name true)
- (assoc :block/type #{"hidden"}
- :block/format :markdown
- :block/properties
- (sqlite-util/build-property-pair :logseq.property/source-page current-page-id))))
- page-tx (when-not page-entity page)
- page-id [:block/uuid (:block/uuid page)]
- parent-id (db/new-block-id)
- parent (-> {:block/uuid parent-id
- :block/format :markdown
- :block/content ""
- :block/page page-id
- :block/parent page-id
- :block/left (or (when page-entity (model/get-block-last-direct-child-id (db/get-db) (:db/id page-entity)))
- page-id)
- :block/properties
- [(sqlite-util/build-property-pair :logseq.property/created-from-block (:db/id block))
- (sqlite-util/build-property-pair :logseq.property/created-from-property (:db/id property))]}
- sqlite-util/block-with-timestamps)
- child-1-id (db/new-block-id)
- child-1 (-> {:block/uuid child-1-id
- :block/format :markdown
- :block/content value
- :block/page page-id
- :block/parent [:block/uuid parent-id]
- :block/left [:block/uuid parent-id]}
- sqlite-util/block-with-timestamps
- parse-block)]
- {:page page-tx
- :blocks [parent child-1]}))
- (defn create-property-text-block!
- [block property value parse-block {:keys [class-schema?]}]
- (assert (e/entity? property))
- (let [repo (state/get-current-repo)
- {:keys [page blocks]} (property-create-new-block block property value parse-block)
- first-block (first blocks)
- last-block-id (:block/uuid (last blocks))
- class? (contains? (:block/type block) "class")
- property-id (:db/ident property)]
- (db/transact! repo (if page (cons page blocks) blocks) {:outliner-op :insert-blocks})
- (let [result (when property-id
- (if (and class? class-schema?)
- (class-add-property! repo (:db/id block) property-id)
- (set-block-property! repo (:db/id block) property-id (:block/uuid first-block) {})))]
- {:last-block-id last-block-id
- :result result})))
- (defn property-create-new-block-from-template
- [block property template]
- (let [current-page-id (:db/id (or (:block/page block) block))
- page-name (str "$$$" current-page-id)
- page-entity (db/get-page page-name)
- page (or page-entity
- (-> (block/page-name->map page-name true)
- (assoc :block/type #{"hidden"}
- :block/format :markdown
- :block/properties
- (sqlite-util/build-property-pair :logseq.property/source-page current-page-id))))
- 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
- :block/left (or (when page-entity (model/get-block-last-direct-child-id (db/get-db) (:db/id page-entity)))
- page-id)
- :block/properties
- [(sqlite-util/build-property-pair :logseq.property/created-from-block [:block/uuid (:block/uuid block)])
- (sqlite-util/build-property-pair :logseq.property/created-from-property (:db/id property))
- (sqlite-util/build-property-pair :logseq.property/created-from-template [:block/uuid (:block/uuid template)])]}
- sqlite-util/block-with-timestamps)]
- {:page page-tx
- :blocks [new-block]}))
- (defn- get-property-hidden-page
- [property]
- (let [page-name (str db-property-util/hidden-page-name-prefix (:block/uuid property))]
- (or (db/get-page page-name)
- (db-property-util/build-property-hidden-page property))))
- (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)))
- (let [schema (-> (:block/schema property)
- (update :values (fn [values]
- (vec (conj (remove #{old-id} values) new-id)))))]
- (db/transact! (state/get-current-repo)
- [{:db/id (:db/id property)
- :block/schema schema}]
- {: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)
- (let [property (db/entity (:db/id property))
- value (if (string? value) (string/trim value) value)
- property-schema (:block/schema property)
- closed-values (:values property-schema)
- block-values (map (fn [id] (db/entity [:block/uuid id])) closed-values)
- 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]))
- value-block (when (uuid? value) (db/entity [:block/uuid value]))
- validate-message (validate-property-value
- (get (built-in-validation-schemas property {:new-closed-value? true}) property-type)
- resolved-value)]
- (cond
- (some (fn [b] (and (= resolved-value (or (db-pu/property-value-when-closed b)
- (:block/uuid b)))
- (not= id (:block/uuid b)))) block-values)
- (do
- (notification/show! "Choice already exists" :warning)
- :value-exists)
- validate-message
- (do
- (notification/show! validate-message :warning)
- :value-invalid)
- (nil? resolved-value)
- nil
- (:block/name value-block) ; page
- (let [new-values (vec (conj closed-values value))]
- {:block-id value
- :tx-data [{:db/id (:db/id property)
- :block/schema (assoc property-schema :values new-values)}]})
- :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 (assoc (:block/schema block)
- :value resolved-value)]
- (cond->
- {:block/uuid id
- :block/schema (if description
- (assoc schema :description description)
- (dissoc schema :description))}
- icon
- (assoc :logseq.property/icon icon)))]
- (let [hidden-tx
- (if (contains? db-property-type/ref-property-types (:type property-schema))
- []
- (let [page (get-property-hidden-page property)
- new-block (db-property-util/build-closed-value-block block-id resolved-value [:block/uuid (:block/uuid page)]
- property {:icon icon
- :description description})]
- (cond-> []
- (not (e/entity? page))
- (conj page)
- true
- (conj new-block))))
- new-values (if (contains? db-property-type/ref-property-types (:type property-schema))
- (vec (conj closed-values (:block/uuid (db/entity resolved-value))))
- (vec (conj closed-values block-id)))]
- (conj hidden-tx
- {:db/id (:db/id property)
- :block/schema (merge {:type property-type}
- (assoc property-schema :values new-values))})))]
- {: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)
- property-schema (:block/schema property)]
- (if (every? uuid? values')
- (p/let [new-value-ids (vec (remove #(nil? (db/entity [:block/uuid %])) values'))]
- (when (seq new-value-ids)
- (let [property-tx {:db/id (:db/id property)
- :block/schema (assoc property-schema :values new-value-ids)}]
- (db/transact! (state/get-current-repo) [property-tx]
- {:outliner-op :insert-blocks})
- new-value-ids)))
- (p/let [property-id (:db/ident property)
- page (get-property-hidden-page property)
- page-tx (when-not (e/entity? page) page)
- page-id (:block/uuid page)
- closed-value-blocks (map (fn [value]
- (db-property-util/build-closed-value-block
- (db/new-block-id)
- value
- [:block/uuid page-id]
- property
- {}))
- values')
- value->block-id (zipmap
- (map #(get-in % [:block/schema :value]) closed-value-blocks)
- (map :block/uuid closed-value-blocks))
- new-value-ids (mapv :block/uuid closed-value-blocks)
- property-tx {:db/id (:db/id property)
- :block/schema (assoc property-schema :values new-value-ids)}
- property-values (db-async/<get-block-property-values (state/get-current-repo) (:db/ident property))
- block-values (->> property-values
- (remove #(uuid? (first %))))
- tx-data (concat
- (when page-tx [page-tx])
- closed-value-blocks
- [property-tx]
- (mapcat (fn [[id value]]
- [[:db/retract id property-id]
- {:db/id id
- property-id (if (set? value)
- (set (map value->block-id value))
- (get value->block-id value))}])
- (filter second block-values)))]
- (db/transact! (state/get-current-repo) tx-data
- {:outliner-op :insert-blocks})
- new-value-ids)))))
- (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)
- (seq (:block/_refs value-block))
- (do (notification/show! "The choice can't be deleted because it's still used." :warning)
- false)
- :else
- (let [property (db/entity (:db/id property))
- schema (:block/schema property)
- tx-data [[:db/retractEntity (:db/id value-block)]
- {:db/id (:db/id property)
- :block/schema (update schema :values
- (fn [values]
- (vec (remove #{(:block/uuid value-block)} values))))}]]
- (p/do!
- (db/transact! tx-data)
- (re-init-commands! property)
- true))))
- (defn get-property-block-created-block
- "Get the root block and property that created this property block."
- [eid]
- (let [b (db/entity eid)
- parents (model/get-block-parents (state/get-current-repo) (:block/uuid b) {})
- [created-from-block created-from-property]
- (some (fn [block]
- (let [from-block (:logseq.property/created-from-block block)
- from-property (:logseq.property/created-from-property block)]
- (when (and from-block from-property)
- [from-block from-property]))) (reverse parents))]
- {:from-block-id (or (:db/id created-from-block) (:db/id b))
- :from-property-id (:db/id created-from-property)}))
- (defn batch-set-property-closed-value!
- [block-ids db-ident closed-value]
- (let [repo (state/get-current-repo)
- property (db/entity db-ident)
- ref-type? (contains? db-property-type/ref-property-types (get-in property [:block/schema :type]))
- closed-value-entity (pu/get-closed-value-entity-by-name db-ident closed-value)
- closed-value-id (if ref-type? (:db/id closed-value-entity) (:block/uuid closed-value-entity))]
- (when closed-value-id
- (batch-set-property! repo
- block-ids
- db-ident
- closed-value-id))))
|