| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105 |
- (ns frontend.components.property.value
- (:require [cljs-time.coerce :as tc]
- [clojure.string :as string]
- [datascript.impl.entity :as de]
- [dommy.core :as d]
- [frontend.components.icon :as icon-component]
- [frontend.components.query.builder :as query-builder-component]
- [frontend.components.select :as select]
- [frontend.components.title :as title]
- [frontend.config :as config]
- [frontend.date :as date]
- [frontend.db :as db]
- [frontend.db-mixins :as db-mixins]
- [frontend.db.async :as db-async]
- [frontend.db.model :as model]
- [frontend.handler.block :as block-handler]
- [frontend.handler.db-based.page :as db-page-handler]
- [frontend.handler.db-based.property :as db-property-handler]
- [frontend.handler.editor :as editor-handler]
- [frontend.handler.page :as page-handler]
- [frontend.handler.property :as property-handler]
- [frontend.handler.property.util :as pu]
- [frontend.handler.route :as route-handler]
- [frontend.modules.outliner.ui :as ui-outliner-tx]
- [frontend.search :as search]
- [frontend.state :as state]
- [frontend.ui :as ui]
- [frontend.util :as util]
- [goog.dom :as gdom]
- [goog.functions :refer [debounce]]
- [lambdaisland.glogi :as log]
- [logseq.common.util :as common-util]
- [logseq.common.util.macro :as macro-util]
- [logseq.db :as ldb]
- [logseq.db.frontend.property :as db-property]
- [logseq.db.frontend.property.type :as db-property-type]
- [logseq.shui.ui :as shui]
- [promesa.core :as p]
- [rum.core :as rum]))
- (rum/defc property-empty-btn-value
- [property & opts]
- (let [text (cond
- (= (:db/ident property) :logseq.property/description)
- "Add description"
- :else
- "Empty")]
- (if (= text "Empty")
- (shui/button (merge {:class "empty-btn" :variant :text} opts)
- text)
- (shui/button (merge {:class "empty-btn !text-base" :variant :text} opts)
- text))))
- (rum/defc property-empty-text-value
- [& {:as opts}]
- [:span.inline-flex.items-center.cursor-pointer
- (merge {:class "empty-text-btn" :variant :text} opts) "Empty"])
- (rum/defc icon-row
- [block editing?]
- (let [icon-value (:logseq.property/icon block)
- clear-overlay! (fn []
- (shui/dialog-close!)
- (shui/popup-hide-all!))
- on-chosen! (fn [_e icon]
- (if icon
- (db-property-handler/set-block-property!
- (:db/id block)
- :logseq.property/icon
- (select-keys icon [:type :id :color]))
- (db-property-handler/remove-block-property!
- (:db/id block)
- :logseq.property/icon))
- (clear-overlay!))]
- (rum/use-effect!
- (fn []
- (when editing?
- (clear-overlay!)
- (let [^js container (or (some-> js/document.activeElement (.closest ".page"))
- (gdom/getElement "main-content-container"))
- icon (get block (pu/get-pid :logseq.property/icon))]
- (util/schedule
- (fn []
- (when-let [^js target (some-> (.querySelector container (str "#ls-block-" (str (:block/uuid block))))
- (.querySelector ".block-main-container"))]
- (shui/popup-show! target
- #(icon-component/icon-search
- {:on-chosen on-chosen!
- :icon-value icon
- :del-btn? (some? icon)})
- {:id :ls-icon-picker
- :align :start})))))))
- [editing?])
- [:div.col-span-3.flex.flex-row.items-center.gap-2
- (icon-component/icon-picker icon-value
- {:disabled? config/publishing?
- :del-btn? (some? icon-value)
- :on-chosen on-chosen!})]))
- (defn- select-type?
- [property type]
- (or (contains? #{:node :number :url :date :page :class :property} type)
- ;; closed values
- (seq (:property/closed-values property))))
- (defn <create-new-block!
- [block property value & {:keys [edit-block?]
- :or {edit-block? true}}]
- (shui/popup-hide!)
- (shui/dialog-close!)
- (p/let [block
- (if (and (= :default (get-in property [:block/schema :type]))
- (not (db-property/many? property)))
- (p/let [existing-value (get block (:db/ident property))
- existing-value? (and (some? existing-value)
- (not= (:db/ident existing-value) :logseq.property/empty-placeholder))
- new-block-id (when-not existing-value? (db/new-block-id))
- _ (when-not existing-value?
- (db-property-handler/create-property-text-block!
- (:db/id block)
- (:db/id property)
- value
- {:new-block-id new-block-id}))]
- (if existing-value? existing-value (db/entity [:block/uuid new-block-id])))
- (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})]
- (db/entity [:block/uuid new-block-id])))]
- (when edit-block?
- (editor-handler/edit-block! block :max {:container-id :unknown-container}))
- block))
- (defn- get-operating-blocks
- [block]
- (let [selected-blocks (some->> (state/get-selection-block-ids)
- (map (fn [id] (db/entity [:block/uuid id])))
- (seq)
- block-handler/get-top-level-blocks
- (remove ldb/property?))]
- (or (seq selected-blocks) [block])))
- (defn <add-property!
- "If a class and in a class schema context, add the property to its schema.
- Otherwise, add a block's property and its value"
- ([block property-key property-value] (<add-property! block property-key property-value {}))
- ([block property-id property-value' {:keys [exit-edit? class-schema?]
- :or {exit-edit? true}}]
- (let [repo (state/get-current-repo)
- class? (ldb/class? block)
- property (db/entity property-id)
- many? (db-property/many? property)
- checkbox? (= :checkbox (get-in property [:block/schema :type]))
- blocks (get-operating-blocks block)]
- (assert (qualified-keyword? property-id) "property to add must be a keyword")
- (p/do!
- (if (and class? class-schema?)
- (db-property-handler/class-add-property! (:db/id block) property-id)
- (let [block-ids (map :block/uuid blocks)]
- (if (and (db-property-type/user-ref-property-types (get-in property [:block/schema :type]))
- (string? property-value'))
- (p/let [new-block (<create-new-block! block (db/entity property-id) property-value' {:edit-block? false})]
- (when (seq (remove #{(:db/id block)} (map :db/id block)))
- (property-handler/batch-set-block-property! repo block-ids property-id (:db/id new-block)))
- new-block)
- (property-handler/batch-set-block-property! repo block-ids property-id property-value'))))
- (when exit-edit?
- (ui/hide-popups-until-preview-popup!)
- (shui/dialog-close!))
- (when-not (or many? checkbox?)
- (when-let [input (state/get-input)]
- (.focus input)))
- (when checkbox?
- (state/set-editor-action-data! {:type :focus-property-value
- :property property}))))))
- (defn- add-or-remove-property-value
- [block property value selected? {:keys [refresh-result-f]}]
- (let [many? (db-property/many? property)
- blocks (get-operating-blocks block)]
- (p/do!
- (if selected?
- (<add-property! block (:db/ident property) value {:exit-edit? (not many?)})
- (p/do!
- (ui-outliner-tx/transact!
- {:outliner-op :save-block}
- (doseq [block blocks]
- (db-property-handler/delete-property-value! (:db/id block) (:db/ident property) value)))
- (when (or (not many?)
- ;; values will be cleared
- (and many? (<= (count (get block (:db/ident property))) 1)))
- (shui/popup-hide!))))
- (when (fn? refresh-result-f) (refresh-result-f)))))
- (rum/defcs calendar-inner <
- (rum/local (str "calendar-inner-" (js/Date.now)) ::identity)
- {:init (fn [state]
- (state/set-editor-action! :property-set-date)
- state)
- :will-mount (fn [state]
- (js/setTimeout
- #(some-> @(::identity state)
- (js/document.getElementById)
- (.querySelector "[aria-selected=true]")
- (.focus)) 16)
- state)
- :will-unmount (fn [state]
- (shui/popup-hide!)
- (shui/dialog-close!)
- (state/set-editor-action! nil)
- state)}
- [state id {:keys [datetime? on-change value del-btn? on-delete]}]
- (let [*ident (::identity state)
- initial-day (or (some-> value (.getTime) (js/Date.)) (js/Date.))
- initial-month (when value
- (js/Date. (.getFullYear value) (.getMonth value)))
- select-handler!
- (fn [^js d]
- ;; force local to UTC
- (when d
- (let [gd (goog.date.Date. (.getFullYear d) (.getMonth d) (.getDate d))]
- (let [journal (date/js-date->journal-title gd)]
- (p/do!
- (when-not (db/get-page journal)
- (page-handler/<create! journal {:redirect? false
- :create-first-block? false}))
- (when (fn? on-change)
- (let [value (if datetime? (tc/to-long d) (db/get-page journal))]
- (on-change value)))
- (shui/popup-hide! id)
- (ui/hide-popups-until-preview-popup!)
- (shui/dialog-close!))))))]
- (ui/nlp-calendar
- (cond->
- {:initial-focus true
- :datetime? datetime?
- :selected initial-day
- :id @*ident
- :del-btn? del-btn?
- :on-delete on-delete
- :on-select select-handler!}
- initial-month
- (assoc :default-month initial-month)))))
- (rum/defc date-picker
- [value {:keys [datetime? on-change on-delete del-btn? editing? multiple-values? other-position?]}]
- (let [*trigger-ref (rum/use-ref nil)
- value' (cond
- (map? value)
- (js/Date. (date/journal-title->long (:block/title value)))
- (number? value)
- (js/Date. value)
- :else
- (let [d (js/Date.)]
- (.setHours d 0 0 0)
- d))
- content-fn (fn [{:keys [id]}] (calendar-inner id
- {:on-change on-change
- :value value'
- :del-btn? del-btn?
- :on-delete on-delete
- :datetime? datetime?}))
- open-popup! (fn [e]
- (when-not (or (util/meta-key? e) (util/shift-key? e))
- (util/stop e)
- (when-not config/publishing?
- (shui/popup-show! (.-target e) content-fn
- {:align "start" :auto-focus? true}))))]
- (rum/use-effect!
- (fn []
- (when editing?
- (js/setTimeout
- #(some-> (rum/deref *trigger-ref)
- (.click)) 32)))
- [editing?])
- (if multiple-values?
- (shui/button
- {:class "jtrigger h-6 empty-btn"
- :ref *trigger-ref
- :variant :text
- :size :sm
- :on-click open-popup!}
- (ui/icon "calendar-plus" {:size 16}))
- (shui/trigger-as
- :div.flex.flex-1.flex-row.gap-1.items-center.flex-wrap
- {:tabIndex 0
- :class "jtrigger min-h-[24px]" ; FIXME: min-h-6 not works
- :ref *trigger-ref
- :on-click open-popup!}
- (cond
- (map? value)
- (when-let [page-cp (state/get-component :block/page-cp)]
- (rum/with-key
- (page-cp {:disable-preview? true
- :meta-click? other-position?} value)
- (:db/id value)))
- (number? value)
- (when-let [date (js/Date. value)]
- [:div.flex.flex-row.gap-1.items-center
- (when-let [page-cp (state/get-component :block/page-cp)]
- (let [page-title (date/journal-name (date/js-date->goog-date date))]
- (page-cp {:disable-preview? true
- :show-non-exists-page? true}
- {:block/name page-title})))
- [:span.opacity-50
- (str (util/zero-pad (.getHours date))
- ":"
- (util/zero-pad (.getMinutes date)))]])
- :else
- (property-empty-btn-value nil))))))
- (rum/defc property-value-date-picker
- [block property value opts]
- (let [multiple-values? (db-property/many? property)
- repo (state/get-current-repo)
- datetime? (= :datetime (get-in property [:block/schema :type]))]
- (date-picker value
- (merge opts
- {:datetime? datetime?
- :multiple-values? multiple-values?
- :on-change (fn [value]
- (property-handler/set-block-property! repo (:block/uuid block)
- (:db/ident property)
- (if (map? value) (:db/id value) value)))
- :del-btn? (some-> value (:block/title) (boolean))
- :on-delete (fn []
- (property-handler/set-block-property! repo (:block/uuid block)
- (:db/ident property) nil)
- (shui/popup-hide!))}))))
- (defn- <create-page-if-not-exists!
- [block property classes page]
- (let [page* (string/trim page)
- ;; inline-class is only for input from :transform-fn
- [page inline-class] (if (and (seq classes) (not (contains? db-property/db-attribute-properties (:db/ident property))))
- (or (seq (map string/trim (rest (re-find #"(.*)#(.*)$" page*))))
- [page* nil])
- [page* nil])
- page-entity (ldb/get-case-page (db/get-db) page)
- id (:db/id page-entity)
- class? (or (= :block/tags (:db/ident property))
- (and (= :logseq.property/parent (:db/ident property))
- (ldb/class? block)))
- ;; Note: property and other types shouldn't be converted to class
- page? (= "page" (:block/type page-entity))]
- (cond
- ;; page not exists or page exists but not a page type
- (or (nil? id) (and class? (not page?)))
- (let [inline-class-uuid
- (when inline-class
- (or (:block/uuid (ldb/get-case-page (db/get-db) inline-class))
- (do (log/error :msg "Given inline class does not exist" :inline-class inline-class)
- nil)))
- create-options {:redirect? false
- :create-first-block? false
- :tags (if inline-class-uuid
- [inline-class-uuid]
- ;; Only 1st class b/c page normally has
- ;; one of and not all these classes
- (mapv :block/uuid (take 1 classes)))}]
- (p/let [page (if class?
- (db-page-handler/<create-class! page create-options)
- (page-handler/<create! page create-options))]
- (:db/id page)))
- (and class? page? id)
- (p/let [_ (db-page-handler/convert-to-tag! page-entity)]
- id)
- :else
- id)))
- (defn- select-aux
- [block property {:keys [items selected-choices multiple-choices?] :as opts}]
- (let [selected-choices (->> selected-choices
- (remove nil?)
- (remove #(= :logseq.property/empty-placeholder %)))
- clear-value (str "No " (:block/title property))
- clear-value-label [:div.flex.flex-row.items-center.gap-2
- (ui/icon "x")
- [:div clear-value]]
- items' (->>
- (if (and (seq selected-choices)
- (not multiple-choices?)
- (not (and (ldb/class? block) (= (:db/ident property) :logseq.property/parent)))
- (not= (:db/ident property) :logseq.property.view/type))
- (concat items
- [{:value clear-value
- :label clear-value-label
- :clear? true}])
- items)
- (remove #(= :logseq.property/empty-placeholder (:value %))))
- k :on-chosen
- f (get opts k)
- f' (fn [chosen selected?]
- (if (or (and (not multiple-choices?) (= chosen clear-value))
- (and multiple-choices? (= chosen [clear-value])))
- (p/do!
- (let [blocks (get-operating-blocks block)
- block-ids (map :block/uuid blocks)]
- (property-handler/batch-remove-block-property!
- (state/get-current-repo)
- block-ids
- (:db/ident property)))
- (shui/popup-hide!))
- (f chosen selected?)))]
- (select/select (assoc opts
- :selected-choices selected-choices
- :items items'
- k f'))
- ;(shui/multi-select-content
- ; (map #(let [{:keys [value label]} %]
- ; {:id value :value label}) items') nil opts)
- ))
- (defn- get-node-icon
- [node]
- (cond
- (ldb/class? node)
- "hash"
- (ldb/property? node)
- "letter-p"
- (ldb/page? node)
- "page"
- :else
- "letter-n"))
- (rum/defcs ^:large-vars/cleanup-todo select-node < rum/reactive db-mixins/query
- (rum/local 0 ::refresh-count)
- [state property
- {:keys [block multiple-choices? dropdown? input-opts on-input] :as opts}
- *result]
- (let [*refresh-count (::refresh-count state)
- ;; Trigger refresh
- _ @*refresh-count
- repo (state/get-current-repo)
- classes (:property/schema.classes property)
- tags? (= :block/tags (:db/ident property))
- alias? (= :block/alias (:db/ident property))
- tags-or-alias? (or tags? alias?)
- block (db/entity (:db/id block))
- result (rum/react *result)
- selected-choices (when block
- (when-let [v (get block (:db/ident property))]
- (if (every? de/entity? v)
- (map :db/id v)
- [(:db/id v)])))
- parent-property? (= (:db/ident property) :logseq.property/parent)
- children-pages (when parent-property? (model/get-structured-children repo (:db/id block)))
- nodes
- (->>
- (cond
- parent-property?
- (let [;; Disallows cyclic hierarchies
- exclude-ids (-> (set (map (fn [id] (:block/uuid (db/entity id))) children-pages))
- (conj (:block/uuid block))) ; break cycle
- options (if (ldb/class? block)
- (model/get-all-classes repo)
- (cond->>
- (->> (model/get-all-pages repo)
- (remove (fn [e] (or (ldb/built-in? e) (ldb/property? e)))))
- (contains? #{"property" "page"} (:block/type block))
- (remove ldb/class?)))
- excluded-options (remove (fn [e] (contains? exclude-ids (:block/uuid e))) options)]
- excluded-options)
- (seq classes)
- (mapcat
- (fn [class]
- (if (= :logseq.class/Root (:db/ident class))
- (model/get-all-classes repo {:except-root-class? true})
- (model/get-class-objects repo (:db/id class))))
- classes)
- :else
- (let [property-type (get-in property [:block/schema :type])]
- (if (empty? result)
- (let [v (get block (:db/ident property))]
- (remove #(= :logseq.property/empty-placeholder (:db/ident %))
- (if (every? de/entity? v) v [v])))
- (remove (fn [node]
- (or (= (:db/id block) (:db/id node))
- ;; A page's alias can't be itself
- (and alias? (= (or (:db/id (:block/page block))
- (:db/id block))
- (:db/id node)))
- (when (and property-type (not= property-type :node))
- (if (= property-type :page)
- (not (db/page? node))
- (not= property-type (some-> (:block/type node) keyword))))))
- result)))))
- options (map (fn [node]
- (let [id (or (:value node) (:db/id node))
- label (if (integer? id)
- (let [title (subs (title/block-unique-title node) 0 256)
- node (or (db/entity id) node)
- icon (get-node-icon node)]
- [:div.flex.flex-col
- (when-not (db/page? node)
- (when-let [breadcrumb (state/get-component :block/breadcrumb)]
- [:div.text-xs.opacity-70.mb-1 {:style {:margin-left 3}}
- (breadcrumb {:search? true} (state/get-current-repo) (:block/uuid block) {})]))
- [:div.flex.flex-row.items-center.gap-1
- (when-not (:property/schema.classes property)
- (ui/icon icon {:size 14}))
- [:div title]]])
- (or (:label node) (:block/title node)))]
- (assoc node
- :label-value (:block/title node)
- :label label
- :value id))) nodes)
- classes' (remove (fn [class] (= :logseq.class/Root (:db/ident class))) classes)
- opts' (cond->
- (merge
- opts
- {:multiple-choices? multiple-choices?
- :items options
- :selected-choices selected-choices
- :dropdown? dropdown?
- :input-default-placeholder (cond
- tags?
- "Set tags"
- alias?
- "Set alias"
- multiple-choices?
- "Choose nodes"
- :else
- "Choose node")
- :show-new-when-not-exact-match? (if (and parent-property? (contains? (set children-pages) (:db/id block)))
- false
- true)
- :extract-chosen-fn :value
- :extract-fn (fn [x] (or (:label-value x) (:label x)))
- :input-opts input-opts
- :on-input (debounce on-input 50)
- :on-chosen (fn [chosen selected?]
- (p/let [[id new?] (if (integer? chosen)
- [chosen false]
- (when-not (string/blank? (string/trim chosen))
- (p/let [result (<create-page-if-not-exists! block property classes' chosen)]
- [result true])))
- _ (when (and (integer? id) (not (ldb/page? (db/entity id))))
- (db-async/<get-block repo id))]
- (p/do!
- (if id
- (add-or-remove-property-value block property id selected? {})
- (log/error :msg "No :db/id found or created for chosen" :chosen chosen))
- (when new? (swap! *refresh-count inc)))))})
- (and (seq classes') (not tags-or-alias?))
- (assoc
- ;; Provides additional completion for inline classes on new pages or objects
- :transform-fn (fn [results input]
- (if-let [[_ new-page class-input] (and (empty? results) (re-find #"(.*)#(.*)$" input))]
- (let [repo (state/get-current-repo)
- descendent-classes (->> classes'
- (mapcat #(model/get-structured-children repo (:db/id %)))
- (map #(db/entity repo %)))]
- (->> (concat classes' descendent-classes)
- (filter #(string/includes? (:block/title %) class-input))
- (mapv (fn [p]
- {:value (str new-page "#" (:block/title p))
- :label (str new-page "#" (:block/title p))}))))
- results))))]
- (select-aux block property opts')))
- (rum/defcs property-value-select-node <
- (rum/local nil ::result)
- [state block property opts
- {:keys [*show-new-property-config?]}]
- (let [*result (::result state)
- input-opts (fn [_]
- {:on-click (fn []
- (when *show-new-property-config?
- (reset! *show-new-property-config? false)))
- :on-key-down
- (fn [e]
- (case (util/ekey e)
- "Escape"
- (when-let [f (:on-chosen opts)] (f))
- nil))})
- opts' (assoc opts
- :block block
- :input-opts input-opts
- :on-input (fn [v]
- (if (string/blank? v)
- (reset! *result nil)
- (p/let [result (search/block-search (state/get-current-repo) v {:enable-snippet? false
- :built-in? false})]
- (reset! *result result)))))]
- (select-node property opts' *result)))
- (rum/defcs select < rum/reactive db-mixins/query
- {:init (fn [state]
- (let [*values (atom :loading)
- refresh-result-f (fn []
- (p/let [result (db-async/<get-block-property-values (state/get-current-repo)
- (:db/ident (nth (:rum/args state) 1)))]
- (reset! *values result)))]
- (refresh-result-f)
- (assoc state
- ::values *values
- ::refresh-result-f refresh-result-f)))}
- [state block property
- {:keys [multiple-choices? dropdown? content-props] :as select-opts}
- {:keys [*show-new-property-config?]}]
- (let [*values (::values state)
- refresh-result-f (::refresh-result-f state)
- values (rum/react *values)
- block (db/sub-block (:db/id block))]
- (when-not (= :loading values)
- (let [schema (:block/schema property)
- type (:type schema)
- closed-values? (seq (:property/closed-values property))
- ref-type? (db-property-type/user-ref-property-types type)
- items (if closed-values?
- (keep (fn [block]
- (let [icon (pu/get-block-property-value block :logseq.property/icon)
- value (db-property/closed-value-content block)]
- {:label (if icon
- [:div.flex.flex-row.gap-1.items-center
- (icon-component/icon icon {:color? true})
- value]
- value)
- :value (:db/id block)
- :label-value value})) (:property/closed-values property))
- (->> values
- (mapcat (fn [value]
- (if (coll? value)
- (map (fn [v] {:value v}) value)
- [{:value value}])))
- (map (fn [{:keys [value]}]
- (if (and ref-type? (number? value))
- (when-let [e (db/entity value)]
- {:label (db-property/property-value-content e)
- :value value})
- {:label value
- :value value})))
- (distinct)))
- items (->> (if (= :date type)
- (map (fn [m] (let [label (:block/title (db/entity (:value m)))]
- (when label
- (assoc m :label label)))) items)
- items)
- (remove nil?))
- on-chosen (fn [chosen selected?]
- (let [value (if (map? chosen) (:value chosen) chosen)]
- (add-or-remove-property-value block property value selected?
- {:refresh-result-f refresh-result-f})))
- selected-choices' (get block (:db/ident property))
- selected-choices (if (every? de/entity? selected-choices')
- (map :db/id selected-choices')
- [selected-choices'])]
- (select-aux block property
- {:multiple-choices? multiple-choices?
- :items items
- :selected-choices selected-choices
- :dropdown? dropdown?
- :show-new-when-not-exact-match? (not (or closed-values? (= :date type)))
- :input-default-placeholder "Select"
- :extract-chosen-fn :value
- :extract-fn (fn [x] (or (:label-value x) (:label x)))
- :content-props content-props
- :on-chosen on-chosen
- :input-opts (fn [_]
- {:on-blur (fn []
- (when-let [f (:on-chosen select-opts)] (f)))
- :on-click (fn []
- (when *show-new-property-config?
- (reset! *show-new-property-config? false)))
- :on-key-down
- (fn [e]
- (case (util/ekey e)
- "Escape"
- (when-let [f (:on-chosen select-opts)] (f))
- nil))})})))))
- (rum/defcs property-normal-block-value <
- {:init (fn [state]
- (assoc state :container-id (state/get-next-container-id)))}
- [state block property value-block]
- (let [container-id (:container-id state)
- multiple-values? (db-property/many? property)
- block-container (state/get-component :block/container)
- blocks-container (state/get-component :block/blocks-container)
- value-block (if (and (coll? value-block) (every? de/entity? value-block))
- (set (remove #(= (:db/ident %) :logseq.property/empty-placeholder) value-block))
- value-block)]
- (if (seq value-block)
- [:div.property-block-container.content.w-full
- (let [config {:id (str (if multiple-values?
- (:block/uuid block)
- (:block/uuid value-block)))
- :container-id container-id
- :editor-box (state/get-component :editor/box)
- :property-block? true}]
- (if (set? value-block)
- (blocks-container config (ldb/sort-by-order value-block))
- (rum/with-key
- (block-container config value-block)
- (str (:db/id property) "-" (:block/uuid value-block)))))]
- [:div
- {:tabIndex 0
- :on-click (fn [] (<create-new-block! block property ""))}
- (property-empty-btn-value property)])))
- (rum/defcs query-cp <
- (rum/local false ::show-setting?)
- [state block property v-block]
- (let [result (common-util/safe-read-string (:block/title v-block))
- advanced-query? (or (and (map? result) (:query result))
- (string/starts-with? (string/triml (:block/title v-block)) "{"))]
- [:div.flex.flex-1.flex-row.gap-1.justify-between
- [:div.flex.flex-1 (property-normal-block-value block property v-block)]
- (when-not advanced-query?
- (shui/button
- {:variant :ghost
- :size :sm
- :class "jtrigger px-1 text-muted-foreground"
- :title "Update query"
- :on-click (fn [e]
- (shui/popup-show!
- (.-target e)
- (fn []
- [:div.p-4.h-64 {:style {:width "42rem"}}
- (let [block (db/entity (:db/id v-block))
- query (:block/title block)]
- (query-builder-component/builder query {:property property
- :block block}))])
- {:align :end}))}
- (ui/icon "settings" {:size 18})))]))
- (rum/defcs property-block-value < rum/reactive db-mixins/query
- {:init (fn [state]
- (let [block (first (:rum/args state))]
- (when-let [block-id (or (:db/id block) (:block/uuid block))]
- (db-async/<get-block (state/get-current-repo) block-id :children? true)))
- state)}
- [state value block property page-cp]
- (when value
- (if (state/sub-async-query-loading value)
- [:div.text-sm.opacity-70 "loading"]
- (if-let [v-block (db/sub-block (:db/id value))]
- (let [class? (ldb/class? v-block)
- invalid-warning [:div.warning.text-sm
- "Invalid block value, please delete the current property."]]
- (when v-block
- (cond
- (= (:db/ident property) :logseq.property/query)
- (query-cp block property v-block)
- (:block/page v-block)
- (property-normal-block-value block property v-block)
- ;; page/class/etc.
- (ldb/page? v-block)
- (rum/with-key
- (page-cp {:disable-preview? true
- :tag? class?} v-block)
- (:db/id v-block))
- :else
- invalid-warning)))
- (property-empty-btn-value property)))))
- (rum/defc closed-value-item < rum/reactive db-mixins/query
- [value {:keys [inline-text icon?]}]
- (when value
- (let [eid (if (de/entity? value) (:db/id value) [:block/uuid value])]
- (when-let [block (db/sub-block (:db/id (db/entity eid)))]
- (let [property-block? (db-property/property-created-block? block)
- value' (db-property/closed-value-content block)
- icon (pu/get-block-property-value block :logseq.property/icon)]
- (cond
- icon
- (if icon?
- (icon-component/icon icon {:color? true})
- [:div.flex.flex-row.items-center.gap-2.h-6
- (icon-component/icon icon {:color? true})
- (when value'
- [:span value'])])
- property-block?
- value'
- (= type :number)
- [:span.number (str value')]
- :else
- (inline-text {} :markdown (str value'))))))))
- (rum/defc select-item
- [property type value {:keys [page-cp inline-text other-position? _icon?] :as opts}]
- (let [closed-values? (seq (:property/closed-values property))
- tag? (or (:tag? opts) (= (:db/ident property) :block/tags))
- inline-text-cp (fn [content]
- [:div.flex.flex-row.items-center
- (inline-text {} :markdown (macro-util/expand-value-if-macro content (state/get-macros)))
- (when (and (= type :url) other-position?)
- (shui/button {:variant :ghost
- :size :sm
- :class "px-0 py-0 h-4"}
- (ui/icon "edit" {:size 14})))])]
- [:div.select-item.cursor-pointer
- (cond
- (= value :logseq.property/empty-placeholder)
- (property-empty-btn-value property)
- (or (ldb/page? value)
- (and (seq (:block/tags value))
- ;; FIXME: page-cp should be renamed to node-cp and
- ;; support this case and maybe other complex cases.
- (not (string/includes? (:block/title value) "[["))))
- (when value
- (rum/with-key
- (page-cp {:disable-preview? true
- :tag? tag?
- :meta-click? other-position?} value)
- (:db/id value)))
- (contains? #{:node :class :property :page} type)
- (when-let [reference (state/get-component :block/reference)]
- (reference {} (:block/uuid value)))
- closed-values?
- (closed-value-item value opts)
- (de/entity? value)
- (when-some [content (if (some? (:property.value/content value))
- ;; content needs to be a string for display purposes
- (str (:property.value/content value))
- (:block/title value))]
- (inline-text-cp content))
- :else
- (inline-text-cp (str value)))]))
- (rum/defc single-value-select
- [block property value value-f select-opts opts]
- (let [*el (rum/use-ref nil)]
- ;; Open popover initially when editing a property
- (rum/use-effect!
- (fn []
- (when (:editing? opts)
- (.click (rum/deref *el))))
- [(:editing? opts)])
- (let [schema (:block/schema property)
- type (get schema :type :default)
- select-opts' (assoc select-opts :multiple-choices? false)
- popup-content (fn content-fn [_]
- [:div.property-select
- (case type
- (:number :url :default)
- (select block property select-opts' opts)
- (:node :class :property :page :date)
- (property-value-select-node block property select-opts' opts))])
- trigger-id (str "trigger-" (:container-id opts) "-" (:db/id block) "-" (:db/id property))
- show! (fn [e]
- (let [target (.-target e)]
- (when-not (or config/publishing?
- (util/shift-key? e)
- (util/meta-key? e)
- (util/link? target)
- (when-let [node (.closest target "a")]
- (not (or (d/has-class? node "page-ref")
- (d/has-class? node "tag")))))
- (shui/popup-show! target popup-content
- {:align "start"
- :as-dropdown? true
- :auto-focus? true
- :trigger-id trigger-id}))))]
- (shui/trigger-as
- (if (:other-position? opts) :div :div.jtrigger.flex.flex-1.w-full)
- {:ref *el
- :id trigger-id
- :tabIndex 0
- :on-click show!}
- (if (string/blank? value)
- (property-empty-text-value)
- (value-f))))))
- (defn- property-value-inner
- [block property value {:keys [inline-text page-cp
- dom-id row?]}]
- (let [schema (:block/schema property)
- multiple-values? (db-property/many? property)
- class (str (when-not row? "flex flex-1 ")
- (when multiple-values? "property-value-content"))
- type (:type schema)]
- [:div.cursor-text
- {:id (or dom-id (random-uuid))
- :tabIndex 0
- :class (str class " " (when-not (= type :default) "jtrigger"))
- :style {:min-height 24}
- :on-click (fn []
- (when (and (= type :default) (nil? value))
- (<create-new-block! block property "")))}
- (cond
- (and (= type :default) (nil? (:block/title value)))
- [:div.jtrigger (property-empty-btn-value property)]
- (= type :default)
- (property-block-value value block property page-cp)
- :else
- (inline-text {} :markdown (macro-util/expand-value-if-macro (str value) (state/get-macros))))]))
- (rum/defcs property-scalar-value < rum/reactive db-mixins/query rum/static
- [state block property value* {:keys [container-id editing? on-chosen]
- :as opts}]
- (let [property (model/sub-block (:db/id property))
- block (db/sub-block (:db/id block))
- schema (:block/schema property)
- type (get schema :type :default)
- editing? (or editing?
- (and (state/sub-editing? [container-id (:block/uuid block)])
- (= (:db/id property) (:db/id (:property (state/get-editor-action-data))))))
- select-type?' (select-type? property type)
- closed-values? (seq (:property/closed-values property))
- select-opts {:on-chosen on-chosen}
- value (if (and (de/entity? value*) (= (:db/ident value*) :logseq.property/empty-placeholder))
- nil
- value*)]
- (if (= :logseq.property/icon (:db/ident property))
- (icon-row block editing?)
- (if (and select-type?'
- (not (and (not closed-values?) (= type :date))))
- (let [value (if (and (nil? value) (= :logseq.property.view/type (:db/ident property)))
- ;; TODO: remove this hack once default value is supported
- (db/entity :logseq.property.view/type.table)
- value)]
- (single-value-select block property value
- (fn [] (select-item property type value opts))
- select-opts
- (assoc opts :editing? editing?)))
- (case type
- (:date :datetime)
- (property-value-date-picker block property value (merge opts {:editing? editing?}))
- :checkbox
- (let [add-property! (fn [] (<add-property! block (:db/ident property) (boolean (not value))))]
- [:label.flex.w-full.as-scalar-value-wrap.cursor-pointer
- (shui/checkbox {:class "jtrigger flex flex-row items-center"
- :disabled config/publishing?
- :auto-focus editing?
- :checked value
- :on-checked-change add-property!
- :on-key-down (fn [e]
- (when (= (util/ekey e) "Enter")
- (add-property!)))})])
- ;; :others
- [:div.flex.flex-1
- (property-value-inner block property value opts)])))))
- (rum/defc multiple-values-inner
- [block property v {:keys [on-chosen editing?] :as opts} schema]
- (let [type (get schema :type :default)
- date? (= type :date)
- *el (rum/use-ref nil)
- items (if (de/entity? v) #{v} v)]
- (rum/use-effect!
- (fn []
- (when editing?
- (.click (rum/deref *el))))
- [editing?])
- (let [select-cp (fn [select-opts]
- (let [select-opts (merge {:multiple-choices? true
- :on-chosen (fn []
- (when on-chosen (on-chosen)))}
- select-opts
- {:dropdown? false})]
- [:div.property-select
- (if (contains? #{:node :page :class :property} type)
- (property-value-select-node block property
- select-opts
- opts)
- (select block property select-opts opts))]))]
- (let [toggle-fn shui/popup-hide!
- content-fn (fn [{:keys [_id content-props]}]
- (select-cp {:content-props content-props}))]
- [:div.multi-values.jtrigger
- {:tab-index "0"
- :ref *el
- :on-click (fn [^js e]
- (let [target (.-target e)]
- (when-not (or (util/link? target) (.closest target "a") config/publishing?)
- (shui/popup-show! (rum/deref *el) content-fn
- {:as-dropdown? true :as-content? false
- :align "start" :auto-focus? true}))))
- :on-key-down (fn [^js e]
- (case (.-key e)
- (" " "Enter")
- (do (some-> (rum/deref *el) (.click))
- (util/stop e))
- :dune))
- :class "flex flex-1 flex-row items-center flex-wrap gap-x-2 gap-y-2 pr-4"}
- (let [not-empty-value? (not= (map :db/ident items) [:logseq.property/empty-placeholder])]
- (if (and (seq items) not-empty-value?)
- (concat
- (->> (for [item items]
- (rum/with-key (select-item property type item opts) (or (:block/uuid item) (str item))))
- (interpose [:span.opacity-50.-ml-2 ","]))
- (when date?
- [(property-value-date-picker block property nil {:toggle-fn toggle-fn})]))
- (if date?
- (property-value-date-picker block property nil {:toggle-fn toggle-fn})
- (property-empty-text-value))))]))))
- (rum/defc multiple-values < rum/reactive db-mixins/query
- [block property opts schema]
- (let [block (db/sub-block (:db/id block))
- value (get block (:db/ident property))
- value' (if (coll? value) value
- (when (some? value) #{value}))]
- (multiple-values-inner block property value' opts schema)))
- (rum/defcs property-value < rum/reactive
- [state block property v {:keys [show-tooltip?]
- :as opts}]
- (ui/catch-error
- (ui/block-error "Something wrong" {})
- (let [block-cp (state/get-component :block/blocks-container)
- properties-cp (state/get-component :block/properties-cp)
- opts (merge opts
- {:page-cp (state/get-component :block/page-cp)
- :inline-text (state/get-component :block/inline-text)
- :editor-box (state/get-component :editor/box)
- :block-cp block-cp
- :properties-cp :properties-cp})
- dom-id (str "ls-property-" (:db/id block) "-" (:db/id property))
- editor-id (str dom-id "-editor")
- schema (:block/schema property)
- type (some-> schema (get :type :default))
- multiple-values? (db-property/many? property)
- v (cond
- (and multiple-values? (or (set? v) (and (coll? v) (empty? v)) (nil? v)))
- v
- multiple-values?
- #{v}
- (set? v)
- (first v)
- :else
- v)
- empty-value? (when (coll? v) (= :logseq.property/empty-placeholder (:db/ident (first v))))
- closed-values? (seq (:property/closed-values property))
- value-cp [:div.property-value-inner
- {:data-type type
- :class (str (when empty-value? "empty-value")
- (when-not (:other-position? opts) " w-full"))}
- (cond
- (= (:db/ident property) :logseq.property.class/properties)
- (properties-cp {} block {:selected? false
- :class-schema? true})
- (and multiple-values? (= type :default) (not closed-values?))
- (property-normal-block-value block property v)
- multiple-values?
- (multiple-values block property opts schema)
- :else
- (let [parent? (= (:db/ident property) :logseq.property/parent)
- value-cp (property-scalar-value block property v
- (merge
- opts
- {:editor-id editor-id
- :dom-id dom-id}))
- page-ancestors (when parent?
- (let [ancestor-pages (loop [parents [block]]
- (if-let [parent (:logseq.property/parent (last parents))]
- (when-not (contains? (set parents) parent)
- (recur (conj parents parent)))
- parents))]
- (->> (reverse ancestor-pages)
- (remove (fn [e] (= (:db/id block) (:db/id e))))
- butlast)))]
- (if (seq page-ancestors)
- [:div.flex.flex-1.items-center.gap-1
- (interpose [:span.opacity-50.text-sm " > "]
- (concat
- (map (fn [{title :block/title :as ancestor}]
- [:a {:on-click #(route-handler/redirect-to-page! (:block/uuid ancestor))} title])
- page-ancestors)
- [value-cp]))]
- value-cp)))]]
- (if show-tooltip?
- (shui/tooltip-provider
- (shui/tooltip
- {:delayDuration 1200}
- (shui/tooltip-trigger
- {:onFocusCapture #(util/stop-propagation %)} value-cp)
- (shui/tooltip-content
- (str "Change " (:block/title property)))))
- value-cp))))
|