Files
logseq/src/main/frontend/components/property/value.cljs
Tienson Qin cbece9e29f fix: lint
2026-01-22 00:52:47 +08:00

1652 lines
82 KiB
Clojure

(ns frontend.components.property.value
(:require [cljs-time.coerce :as tc]
[cljs-time.core :as t]
[clojure.set :as set]
[clojure.string :as string]
[dommy.core :as d]
[frontend.components.icon :as icon-component]
[frontend.components.select :as select]
[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.notification :as notification]
[frontend.handler.page :as page-handler]
[frontend.handler.property :as property-handler]
[frontend.handler.property.util :as pu]
[frontend.handler.publish :as publish-handler]
[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]
[frontend.util.cursor :as cursor]
[goog.functions :refer [debounce]]
[lambdaisland.glogi :as log]
[logseq.common.util.macro :as macro-util]
[logseq.db :as ldb]
[logseq.db.frontend.content :as db-content]
[logseq.db.frontend.entity-util :as entity-util]
[logseq.db.frontend.property :as db-property]
[logseq.db.frontend.property.type :as db-property-type]
[logseq.outliner.property :as outliner-property]
[logseq.shui.hooks :as hooks]
[logseq.shui.ui :as shui]
[promesa.core :as p]
[rum.core :as rum]))
(defonce string-value-on-click
{:logseq.property.asset/external-url
(fn [block property]
(when-not (string/starts-with? (get block (:db/ident property)) "zotero://")
(state/pub-event! [:asset/dialog-edit-external-url block])))})
(defn- entity-map?
[m]
(and (map? m) (:db/id m)))
(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" :variant :text} opts)
text))))
(rum/defc property-empty-text-value
[property {:keys [property-position table-view?]}]
[:span.inline-flex.items-center.cursor-pointer.w-full
(merge {:class "empty-text-btn" :variant :text})
(when-not table-view?
(if property-position
(if-let [icon (:logseq.property/icon property)]
(icon-component/icon icon {:color? true})
(ui/icon "line-dashed"))
"Empty"))])
(defn- get-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?)))
(defn get-operating-blocks
[block]
(let [selected-blocks (get-selected-blocks)
view-selected-blocks (:view/selected-blocks @state/state)]
(or (seq selected-blocks)
(seq view-selected-blocks)
[block])))
(defn batch-operation?
[]
(let [selected-blocks (get-selected-blocks)
view-selected-blocks (:view/selected-blocks @state/state)]
(or (> (count selected-blocks) 1)
(seq view-selected-blocks))))
(rum/defc icon-row
[block editing?]
(hooks/use-effect!
(fn []
(fn []
(when editing?
(editor-handler/restore-last-saved-cursor!)))))
(let [icon-value (:logseq.property/icon block)
clear-overlay! (fn []
(shui/popup-hide-all!))
on-chosen! (fn [_e icon]
(let [blocks (get-operating-blocks block)]
(property-handler/batch-set-block-property!
(map :db/id blocks)
:logseq.property/icon
(when icon (select-keys icon [:type :id :color]))))
(clear-overlay!)
(when editing?
(editor-handler/restore-last-saved-cursor!)))
icon (get block :logseq.property/icon)]
(if editing?
(icon-component/icon-search
{:on-chosen on-chosen!
:icon-value icon
:del-btn? (some? icon)})
[: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?
[block property]
(let [type (:logseq.property/type property)]
(or (contains? #{:node :number :date :page :class :property} type)
;; closed values
(seq (:property/closed-values property))
(and (= (:db/ident property) :logseq.property/default-value)
(= (:logseq.property/type block) :number)))))
(defn <create-new-block!
[block property value & {:keys [edit-block? batch-op?]
:or {edit-block? true}}]
(when-not (or (:logseq.property/hide? property)
(= (:db/ident property) :logseq.property/default-value))
(ui/hide-popups-until-preview-popup!))
(let [<create-block (fn [block]
(if (and (contains? #{:default :url} (:logseq.property/type property))
(not (db-property/many? property)))
(p/let [default-value (:logseq.property/default-value property)
new-block-id (db/new-block-id)
_ (let [value' (if (and default-value (string? value) (string/blank? value))
(db-property/property-value-content default-value)
value)]
(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]))
(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]))))]
(p/let [blocks (if batch-op?
(p/all (map <create-block (get-operating-blocks block)))
(p/let [new-block (<create-block block)]
[new-block]))]
(let [first-block (first blocks)]
(when edit-block?
(editor-handler/edit-block! first-block :max {:container-id :unknown-container}))
first-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-id property-value] (<add-property! block property-id property-value {}))
([block property-id property-value {:keys [selected? exit-edit? class-schema? entity-id?]
:or {exit-edit? true}}]
(let [class? (ldb/class? block)
property (db/entity property-id)
many? (db-property/many? property)
checkbox? (= :checkbox (:logseq.property/type property))
blocks (get-operating-blocks block)]
(when-not (ldb/class? property)
(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)
set-query-list-view? (and (:logseq.property/query block)
(= property-id :logseq.property.view/type)
(= property-value (:db/id (db/entity :logseq.property.view/type.list))))]
(ui-outliner-tx/transact!
{:outliner-op :set-block-property}
(property-handler/batch-set-block-property! block-ids property-id property-value {:entity-id? entity-id?})
(when (and set-query-list-view?
(nil? (:logseq.property.view/group-by-property block)))
(property-handler/batch-set-block-property! block-ids :logseq.property.view/group-by-property
(:db/id (db/entity :block/page))
{:entity-id? entity-id?})))))
(when (seq (:view/selected-blocks @state/state))
(notification/show! "Property updated!" :success))
(when-not many?
(cond
exit-edit?
(ui/hide-popups-until-preview-popup!)
selected?
(shui/popup-hide!)))
(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 entity-id?] :as opts}]
(let [many? (db-property/many? property)
blocks (get-operating-blocks block)
repo (state/get-current-repo)]
(p/do!
(db-async/<get-block repo (:db/id block) {:children? false})
(when (and selected?
(= :db.type/ref (:db/valueType property))
(number? value)
(not (db/entity value)))
(db-async/<get-block repo value {:children? false}))
(if selected?
(<add-property! block (:db/ident property) value
{:selected? selected?
:entity-id? entity-id?
:exit-edit? (if (some? (:exit-edit? opts)) (:exit-edit? opts) (not many?))})
(p/do!
(ui-outliner-tx/transact!
{:outliner-op :save-block}
(db-property-handler/batch-delete-property-value! (map :db/id blocks) (: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)))))
(declare property-value)
(rum/defc repeat-setting < rum/reactive db-mixins/query
[block property]
(let [opts {:exit-edit? false}
block (db/sub-block (:db/id block))]
[:div.p-4.hidden.sm:flex.flex-col.gap-4.w-64
[:div.mb-4
[:div.flex.flex-row.items-center.gap-1
[:div.w-4
(property-value block (db/entity :logseq.property.repeat/repeated?)
(assoc opts
:on-checked-change (fn [value]
(if value
(db-property-handler/set-block-property! (:db/id block)
:logseq.property.repeat/temporal-property
(:db/id property))
(db-property-handler/remove-block-property! (:db/id block)
:logseq.property.repeat/temporal-property)))))]
(if (#{:logseq.property/deadline :logseq.property/scheduled} (:db/ident property))
[:div "Repeat task"]
[:div "Repeat " (if (= :date (:logseq.property/type property)) "date" "datetime")])]]
[:div.flex.flex-row.gap-2.ls-repeat-task-frequency
[:div.flex.text-muted-foreground
"Every"]
;; recur frequency
[:div.w-10.mr-2
(property-value block (db/entity :logseq.property.repeat/recur-frequency) opts)]
;; recur unit
[:div.w-20
(property-value block (db/entity :logseq.property.repeat/recur-unit) (assoc opts :property property))]]
(let [properties (->>
(outliner-property/get-block-full-properties (db/get-db) (:db/id block))
(filter (fn [property]
(and (not (ldb/built-in? property))
(>= (count (:property/closed-values property)) 2))))
(concat [(db/entity :logseq.property/status)])
(util/distinct-by :db/id))
status-property (or (:logseq.property.repeat/checked-property block)
(db/entity :logseq.property/status))
property-id (:db/id status-property)
done-choice (or
(some (fn [choice] (when (true? (:logseq.property/choice-checkbox-state choice)) choice)) (:property/closed-values status-property))
(db/entity :logseq.property/status.done))]
[:div.flex.flex-col.gap-2
[:div.text-muted-foreground
"When"]
(shui/select
(cond->
{:on-value-change (fn [v]
(db-property-handler/set-block-property! (:db/id block)
:logseq.property.repeat/checked-property
v))}
property-id
(assoc :default-value property-id))
(shui/select-trigger
(shui/select-value {:placeholder "Select a property"}))
(shui/select-content
(map (fn [choice]
(shui/select-item {:key (str (:db/id choice))
:value (:db/id choice)} (:block/title choice))) properties)))
[:div.flex.flex-row.gap-1
[:div.text-muted-foreground
"is:"]
(when done-choice
(db-property/property-value-content done-choice))]])]))
(rum/defcs calendar-inner < rum/reactive db-mixins/query
(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!)
(state/set-editor-action! nil)
state)}
[state id {:keys [block property datetime? on-change del-btn? on-delete]}]
(let [block (db/sub-block (:db/id block))
value (get block (:db/ident property))
value (cond
(map? value)
(when-let [day (:block/journal-day value)]
(let [t (date/journal-day->utc-ms day)]
(js/Date. t)))
(number? value)
(js/Date. value)
:else
(let [d (js/Date.)]
(.setHours d 0 0 0)
d))
*ident (::identity state)
initial-day value
initial-month (when value
(let [d (tc/to-date-time value)]
(js/Date. (t/last-day-of-the-month (t/date-time (t/year d) (t/month d))))))
select-handler!
(fn [^js d]
(when d
(let [journal (date/js-date->journal-title d)]
(p/do!
(when-not (model/get-journal-page journal)
(page-handler/<create! journal {:redirect? false}))
(when (fn? on-change)
(let [value (if datetime? (tc/to-long d) (model/get-journal-page journal))]
(on-change value)))
(when-not datetime?
(shui/popup-hide! id)
(ui/hide-popups-until-preview-popup!))))))]
[:div.flex.flex-row.gap-2
[:div.flex.flex-1.items-center
(ui/nlp-calendar
(cond->
{:initial-focus true
:datetime? datetime?
:selected initial-day
:id @*ident
:del-btn? del-btn?
:on-delete on-delete
:on-day-click select-handler!}
initial-month
(assoc :default-month initial-month)))]
[:div.hidden.sm:initial
(shui/separator {:orientation "vertical"})]
(repeat-setting block property)]))
(rum/defc overdue
[date content]
(let [[current-time set-current-time!] (hooks/use-state (t/now))]
(hooks/use-effect!
(fn []
(let [timer (js/setInterval (fn [] (set-current-time! (t/now))) (* 1000 60 3))]
#(js/clearInterval timer)))
[])
(let [overdue? (when date (t/after? current-time (t/plus date (t/seconds 59))))]
[:div
(cond-> {} overdue? (assoc :class "overdue"
:title "Overdue"))
content])))
(defn- start-of-local-day [^js d]
;; clone the date and reset to local midnight
(doto (js/Date. d)
(.setHours 0 0 0 0)))
(defn- human-date-label [utc-ms]
;; utc-ms is stored deadline/scheduled time
(let [given-date (start-of-local-day (js/Date. utc-ms))
today (start-of-local-day (js/Date.))
ms-in-day (* 24 60 60 1000)
tomorrow (js/Date. (+ (.getTime today) ms-in-day))
yesterday (js/Date. (- (.getTime today) ms-in-day))]
(cond
(= (.getTime given-date) (.getTime yesterday)) "Yesterday"
(= (.getTime given-date) (.getTime today)) "Today"
(= (.getTime given-date) (.getTime tomorrow)) "Tomorrow"
:else nil)))
(rum/defc datetime-value
[value property-id repeated-task?]
(when-let [date (t/to-default-time-zone (tc/from-long value))]
(let [content [:div.ls-datetime.flex.flex-row.gap-1.items-center
(when-let [page-cp (state/get-component :block/page-cp)]
(let [page-title (date/journal-name date)]
(rum/with-key
(page-cp {:disable-preview? true
:show-non-exists-page? true
:label (human-date-label value)}
{:block/name page-title})
page-title)))
(let [date (js/Date. value)
hours (.getHours date)
minutes (.getMinutes date)]
[:span.select-none
(if (= 0 hours minutes)
(ui/icon "edit" {:size 14 :class "text-muted-foreground hover:text-foreground align-middle"})
(str (util/zero-pad hours)
":"
(util/zero-pad minutes)))])]]
(if (or repeated-task? (contains? #{:logseq.property/deadline :logseq.property/scheduled} property-id))
(overdue date content)
content))))
(defn- delete-block-property!
[block property]
(editor-handler/move-cross-boundary-up-down :up {})
(property-handler/remove-block-property! (:db/id block)
(:db/ident property)))
(rum/defc date-picker
[value {:keys [block property datetime? on-change on-delete del-btn? editing? multiple-values? other-position?]}]
(let [*el (hooks/use-ref nil)
content-fn (fn [{:keys [id]}] (calendar-inner id
{:block block
:property property
: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)
(editor-handler/save-current-block!)
(when-not config/publishing?
(shui/popup-show! (.-target e) content-fn
{:align "start" :auto-focus? true}))))
repeated-task? (:logseq.property.repeat/repeated? block)]
(if editing?
(content-fn {:id :date-picker})
(if multiple-values?
(shui/button
{:class "jtrigger h-6 empty-btn"
:variant :text
:size :sm
:on-click open-popup!
:on-key-down (fn [e]
(when (contains? #{"Backspace" "Delete"} (util/ekey e))
(delete-block-property! block property)))}
(ui/icon "calendar-plus" {:size 16}))
(shui/trigger-as
:div.flex.flex-1.flex-row.gap-1.items-center.flex-wrap
{:ref *el
:tabIndex 0
:class "jtrigger min-h-[24px]" ; FIXME: min-h-6 not works
:on-click open-popup!
:on-key-down (fn [e]
(case (util/ekey e)
("Backspace" "Delete")
(delete-block-property! block property)
(" " "Enter")
(do (some-> (rum/deref *el) (.click))
(util/stop e))
nil))}
[:div.flex.flex-row.gap-1.items-center
(when repeated-task?
(ui/icon "repeat" {:size 14 :class "opacity-40"}))
(cond
(map? value)
(let [date (tc/to-date-time (date/journal-day->utc-ms (:block/journal-day value)))
compare-value (some-> date
(t/plus (t/days 1))
(t/minus (t/seconds 1)))
content (when-let [page-cp (state/get-component :block/page-cp)]
(rum/with-key
(page-cp {:disable-preview? true
:meta-click? other-position?
:label (human-date-label value)} value)
(:db/id value)))]
(if (or repeated-task? (contains? #{:logseq.property/deadline :logseq.property/scheduled} (:db/id property)))
(overdue compare-value content)
content))
(number? value)
(datetime-value value (:db/ident property) repeated-task?)
:else
(property-empty-btn-value nil))])))))
(rum/defc property-value-date-picker
[block property value opts]
(let [multiple-values? (db-property/many? property)
datetime? (= :datetime (:logseq.property/type property))]
(date-picker value
(merge opts
{:block block
:property property
:datetime? datetime?
:multiple-values? multiple-values?
:on-change (fn [value]
(let [blocks (get-operating-blocks block)]
(property-handler/batch-set-block-property! (map :block/uuid blocks)
(:db/ident property)
(if datetime?
value
(:db/id value)))))
:del-btn? (some? value)
:on-delete (fn [e]
(util/stop-propagation e)
(let [blocks (get-operating-blocks block)]
(property-handler/batch-set-block-property! (map :block/uuid blocks)
(: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.class/extends (:db/ident property))
(ldb/class? block))
(let [classes (:logseq.property/classes property)]
(and (seq classes)
(every? (fn [class]
(or
(= :logseq.class/Tag (:db/ident class))
(some (fn [e]
(= :logseq.class/Tag (:db/ident e)))
(ldb/get-class-extends class))))
classes))))
;; Note: property and other types shouldn't be converted to class
page? (ldb/internal-page? 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
:tags (if inline-class-uuid
[inline-class-uuid]
;; Only 1st class b/c page normally has
;; one of and not all these classes
(let [tag (db/entity :logseq.class/Tag)
classes' (if (= (map :db/id classes) [(:db/id tag)])
classes
(->> (remove (fn [c] (= (:db/id c) (:db/id tag))) classes)
(take 1)))]
(mapv :block/uuid 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-page-to-tag! page-entity)]
id)
:else
id)))
(defn- sort-select-items
[property selected-choices items]
(if (:property/closed-values property)
items ; sorted by order
(sort-by
(juxt (fn [item] (not (selected-choices (:db/id item))))
db-property/property-value-content)
items)))
(rum/defc select-aux
[block property {:keys [items selected-choices multiple-choices?] :as opts}]
(let [selected-choices (->> selected-choices
(remove nil?)
(remove #(= :logseq.property/empty-placeholder %))
set)
clear-value (str "No " (:block/title property))
clear-value-label [:div.flex.flex-row.items-center.gap-1.text-sm
(ui/icon "x" {:size 14})
[:div clear-value]]
[sorted-items set-items!] (hooks/use-state (sort-select-items property selected-choices items))
items' (->>
(if (and (seq selected-choices)
(not multiple-choices?)
(not (and (ldb/class? block) (= (:db/ident property) :logseq.property.class/extends)))
(not= (:db/ident property) :logseq.property.view/type))
(concat sorted-items
(when-not (or (= (:logseq.property/default-value property)
(get block (:db/ident property)))
(= (:logseq.property/scalar-default-value property)
(get block (:db/ident property))))
[{:value clear-value
:label clear-value-label
:clear? true}]))
sorted-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!
block-ids
(:db/ident property)))
(when-not (false? (:exit-edit? opts))
(shui/popup-hide!)))
(f chosen selected?)))]
(hooks/use-effect!
(fn []
(when-not (= (count items) (count sorted-items))
(set-items! (sort-select-items property selected-choices items))))
[items])
(select/select (assoc opts
:selected-choices selected-choices
:items items'
:close-modal? false
k f'))))
(rum/defc ^:large-vars/cleanup-todo select-node < rum/static
[property
{:keys [block multiple-choices? dropdown? input-opts on-input add-new-choice! target] :as opts}
result]
(let [[*input set-input!] (hooks/use-state nil)
repo (state/get-current-repo)
classes (:logseq.property/classes property)
tags? (= :block/tags (:db/ident property))
alias? (= :block/alias (:db/ident property))
tags-or-alias? (or tags? alias?)
block (or (db/entity (:db/id block)) block)
selected-choices (when block
(when-let [v (get block (:db/ident property))]
(if (every? entity-map? v)
(map :db/id v)
[(:db/id v)])))
extends-property? (= (:db/ident property) :logseq.property.class/extends)
children-pages (when extends-property? (model/get-structured-children repo (:db/id block)))
property-type (:logseq.property/type property)
nodes (cond
extends-property?
(let [extends (->> (mapcat (fn [e] (ldb/get-class-extends e)) (:logseq.property.class/extends block))
distinct)
;; Disallows cyclic hierarchies
exclude-ids (-> (set (map (fn [id] (:block/uuid (db/entity id))) children-pages))
(conj (:block/uuid block)) ; break cycle
;; hide parent extends for existing values
(set/union (set (map :block/uuid extends))))
options (if (ldb/class? block)
(model/get-all-classes repo {:except-extends-hidden-tags? true})
result)
excluded-options (->> options
(remove (fn [e] (contains? exclude-ids (:block/uuid e)))))]
excluded-options)
(= :class property-type)
(cond->
(model/get-all-classes
repo
{:except-root-class? true
:except-private-tags? (not (contains? #{:logseq.property/template-applied-to} (:db/ident property)))})
(not (or (and (entity-util/page? block) (not (ldb/internal-page? block))) (:logseq.property/created-from-property block)))
(conj (db/entity :logseq.class/Page)))
(= :property property-type)
(property-handler/get-class-property-choices)
(seq classes)
(->>
(mapcat
(fn [class]
(model/get-class-objects repo (:db/id class)))
classes)
distinct)
:else
(if (empty? result)
(let [v (get block (:db/ident property))]
(remove #(= :logseq.property/empty-placeholder (:db/ident %))
(if (every? entity-map? v) v [v])))
(remove (fn [node]
(let [node' (if (:value node)
(assoc (:value node) :block/title (:label node))
node)
node (or (some-> (:db/id node') db/entity) 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)))
(= :logseq.property/empty-placeholder (:db/ident node))
(cond
(= property-type :class)
(ldb/private-tags (:db/ident node))
(and property-type (not= property-type :node))
(if (= property-type :page)
(not (db/page? node))
(not (contains? (ldb/get-entity-types node) property-type)))
:else
false))))
result)))
options (map (fn [node]
(let [node (if (:value node)
(assoc (:value node) :block/title (:label node))
node)
id (:db/id node)
[header label] (if (integer? id)
(when-let [title (if (seq (:logseq.property/classes property))
(some-> (db-content/recur-replace-uuid-in-block-title node)
(subs 0 256))
(block-handler/block-unique-title node))]
(let [node (or (db/entity id) node)
header (when-not (db/page? node)
(when-let [breadcrumb (state/get-component :block/breadcrumb)]
[:div.text-xs.opacity-70
(breadcrumb {:search? true} (state/get-current-repo) (:block/uuid node)
{:disabled? true})]))
label [:div.flex.flex-row.items-center.gap-1
(when-not (or (:logseq.property/classes property)
(contains? #{:class :property} property-type))
(icon-component/get-node-icon-cp node {:ignore-current-icon? true}))
[:div (if (contains? #{:class :property :page} property-type)
title
(block-handler/block-title-with-icon node title icon-component/icon))]]]
[header label]))
[nil (:block/title node)])]
(assoc node
:header header
:label-value (:block/title node)
:label label
:value id
:disabled? (and tags? (contains?
(set/union #{:logseq.class/Journal :logseq.class/Whiteboard}
(set/difference ldb/internal-tags #{:logseq.class/Page}))
(:db/ident node)))))) nodes)
classes' (remove (fn [class] (= :logseq.class/Root (:db/ident class))) classes)
opts' (cond->
(merge
opts
{:multiple-choices? multiple-choices?
:tap-*input-val set-input!
:items options
:selected-choices selected-choices
:dropdown? dropdown?
:input-default-placeholder (cond
tags?
"Set tags"
alias?
"Set alias"
:else
(str "Set " (:block/title property)))
:show-new-when-not-exact-match? (not
(or (and extends-property?
(or (contains? (set children-pages) (:db/id block))
(when-let [input (when *input @*input)]
(when-not (string/blank? input)
(some (fn [ident]
(= input (:block/title (db/entity ident)))) ldb/extends-hidden-tags)))))
;; Don't allow creating private tags
(and (= :block/tags (:db/ident property))
(seq (set/intersection (set (map :db/ident classes'))
ldb/private-tags)))))
: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 [add-tag-property? (and (= (:db/ident property) :logseq.property.class/properties) (not (integer? chosen)))
id (if (integer? chosen)
chosen
(when-not (string/blank? (string/trim chosen))
(if (= (:db/ident property) :logseq.property.class/properties)
(do
(shui/popup-hide!)
(state/pub-event! [:editor/new-property {:block block
:class-schema? true
:property-key chosen
:target target}]))
(<create-page-if-not-exists! block property classes' chosen))))
_ (when (and (integer? id) (not (entity-util/page? (db/entity id))))
(db-async/<get-block repo id))]
(if id
(p/do!
(add-or-remove-property-value block property id selected? {})
(when (fn? add-new-choice!)
(add-new-choice!
(let [e (db/entity id)]
{:value (select-keys e [:db/id :block/uuid])
:label (:block/title e)}))))
(when-not add-tag-property?
(log/error :msg "No :db/id found or created for chosen" :chosen chosen)))))})
(= :block/tags (:db/ident property))
(assoc :exact-match-exclude-items
(set (map (fn [ident] (:block/title (db/entity ident))) ldb/private-tags)))
(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/defc property-value-select-node < rum/static
[block property opts
{:keys [*show-new-property-config?]}]
(let [[initial-choices set-initial-choices!] (hooks/use-state nil)
[result set-result!] (hooks/use-state nil)
set-result-and-initial-choices! (fn [value]
(set-initial-choices! value)
(set-result! value))
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)
(set-result! initial-choices)
;; TODO rank initial choices higher
(p/let [result (search/block-search (state/get-current-repo) v {:enable-snippet? false
:built-in? false})]
(set-result! result))))
:add-new-choice! (fn [new-choice]
(set-initial-choices! (conj (vec initial-choices) new-choice))))
repo (state/get-current-repo)
classes (:logseq.property/classes property)
class? (= :class (:logseq.property/type property))
non-root-classes (cond-> (remove (fn [c] (= (:db/ident c) :logseq.class/Root)) classes)
class?
(conj (frontend.db/entity :logseq.class/Tag)))
extends-property? (= (:db/ident property) :logseq.property.class/extends)]
;; effect runs once
(hooks/use-effect!
(fn []
(cond
extends-property?
nil
(seq non-root-classes)
(p/let [result (p/all (map (fn [class] (db-async/<get-tag-objects repo (:db/id class))) non-root-classes))
result' (distinct (apply concat result))]
(set-result-and-initial-choices! result'))
:else
(p/let [result (db-async/<get-property-values (:db/ident property))]
(set-result-and-initial-choices! 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 []
(let [[block property _] (:rum/args state)]
(p/let [property-ident (if (= :logseq.property/default-value (:db/ident property))
(:db/ident block)
(:db/ident property))
result (db-async/<get-property-values property-ident)]
(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? exit-edit?] :as opts}]
(let [*values (::values state)
refresh-result-f (::refresh-result-f state)
values (rum/react *values)]
(when-not (= :loading values)
(let [type (:logseq.property/type property)
closed-values? (seq (:property/closed-values property))
items (if closed-values?
(let [date? (and
(= (:db/ident property) :logseq.property.repeat/recur-unit)
(= :date (:logseq.property/type (:property opts))))
values (cond->> (db-property/scoped-closed-values property block)
date?
(remove (fn [b] (contains? #{:logseq.property.repeat/recur-unit.minute :logseq.property.repeat/recur-unit.hour} (:db/ident b)))))]
(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}))
values))
(->> values
(map (fn [{:keys [value label]}]
{:label label
:value (:db/id value)}))
(distinct)))
items (->> (cond
(= :checkbox type)
[{:label "True"
:value true}
{:label "False"
:value false}]
(= :date type)
(map (fn [m] (let [label (:block/title (db/entity (:value m)))]
(when label
(assoc m :label label)))) items)
:else
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?
{:entity-id? (when (integer? value) true)
:exit-edit? exit-edit?
:refresh-result-f refresh-result-f})))
selected-choices' (get block (:db/ident property))
selected-choices (when-not (= type :checkbox)
(if (every? #(and (map? %) (:db/id %)) 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 (str "Set " (:block/title property))
: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 opts]
(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? entity-map? value-block))
(set (remove #(= (:db/ident %) :logseq.property/empty-placeholder) value-block))
value-block)
default-value (:logseq.property/default-value property)
default-value? (and
(:db/id default-value)
(= (:db/id value-block) (:db/id default-value))
(not= (:db/ident property) :logseq.property/default-value))
table-text-property-render (:table-text-property-render opts)]
(if table-text-property-render
(table-text-property-render
value-block
{:create-new-block #(<create-new-block! block property "")
:property-ident (:db/ident property)})
(cond
(seq value-block)
[:div.property-block-container.content.w-full
{:style (if (= (:db/ident property) :logseq.property/default-value)
{:min-width 300}
{})}
(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
:on-block-content-pointer-down (when default-value?
(fn [_e]
(<create-new-block! block property (or (:block/title default-value) ""))))
:p-block (:db/id block)
:p-property (:db/id property)
:view? (:view? opts)}]
(if (set? value-block)
(blocks-container config (ldb/sort-by-order value-block))
(rum/with-key
(block-container (assoc config
:block/uuid (:block/uuid value-block)
:property-default-value? default-value?) value-block)
(str (:db/id block) "-" (:db/id property) "-" (:db/id value-block)))))]
:else
[:div.w-full.h-full.jtrigger.ls-empty-text-property.text-muted-foreground
{:tabIndex 0
:class (if (:table-view? opts) "cursor-pointer" "cursor-text")
:style {:min-height 20 :margin-left 3}
:on-click #(<create-new-block! block property "")}
(when (:class-schema? opts)
"Add description")]))))
(rum/defc property-block-value
[value block property page-cp opts]
(let [v-block value
class? (ldb/class? v-block)]
(cond
(entity-util/page? v-block)
(rum/with-key
(page-cp {:disable-preview? true
:tag? class?
:with-tags? false} v-block)
(:db/id v-block))
:else
(property-normal-block-value block property v-block opts))))
(rum/defc single-string-input
[block property value table-view?]
(let [[editing? set-editing!] (hooks/use-state false)
*ref (hooks/use-ref nil)
string-value (cond
(string? value) value
(some? value) (str (db-property/property-value-content value))
:else "")
[value set-value!] (hooks/use-state string-value)
set-property-value! (fn [value & {:keys [exit-editing?]
:or {exit-editing? true}}]
(let [next-value (or value "")
blank? (string/blank? next-value)]
(p/do!
(if blank?
(when (get block (:db/ident property))
(db-property-handler/remove-block-property! (:db/id block) (:db/ident property)))
(when (not= string-value next-value)
(db-property-handler/set-block-property! (:db/id block)
(:db/ident property)
next-value)))
(set-value! (or (get (db/entity (:db/id block)) (:db/ident property)) ""))
(when exit-editing?
(set-editing! false)))))]
(hooks/use-effect!
(fn []
(set-value! string-value)
#())
[string-value])
[:div.ls-string.flex.flex-1.jtrigger
{:ref *ref
:on-click #(do
(state/clear-selection!)
(set-editing! true))}
(if editing?
(shui/input
{:auto-focus true
:class (str "ls-string-input h-6 px-0 py-0 border-none bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 text-base"
(when table-view? " text-sm"))
:value value
:on-change (fn [e]
(set-value! (util/evalue e)))
:on-blur (fn [_e]
(p/do!
(set-property-value! value)))
:on-key-down (fn [e]
(case (util/ekey e)
"Enter"
(do
(util/stop e)
(set-property-value! value))
"Escape"
(do
(util/stop e)
(set-value! string-value)
(set-editing! false)
(some-> (rum/deref *ref) (.focus)))
nil))})
(if (string/blank? string-value)
(property-empty-text-value property {:table-view? table-view?})
string-value))]))
(rum/defc closed-value-item < rum/reactive db-mixins/query
[value {:keys [inline-text icon?]}]
(when value
(let [eid (if (entity-map? value) (:db/id value) [:block/uuid value])
block (or (db/sub-block (:db/id (db/entity eid))) value)
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-1.h-6
(icon-component/icon icon {:color? true})
(when value'
[:span value'])])
property-block?
value'
(= type :number)
[:span.number (str value')]
:else
[:span.inline-flex.w-full
(let [value' (str value')
value' (if (string/blank? value')
"Empty"
value')]
(inline-text {} :markdown value'))]))))
(rum/defc select-item
[property type value {:keys [page-cp inline-text other-position? property-position table-view? _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)))])]
[:div.select-item.cursor-pointer
(cond
(= value :logseq.property/empty-placeholder)
(property-empty-btn-value property)
closed-values?
(closed-value-item value opts)
(or (entity-util/page? value)
(seq (:block/tags value)))
(when value
(let [opts {:disable-preview? true
:tag? tag?
:with-tags? false
:property-position property-position
:other-position? other-position?
:table-view? table-view?
:ignore-alias? (= :block/alias (:db/ident property))
:on-context-menu
(fn [e]
(util/stop e)
(shui/popup-show! (.-target e)
(fn []
[:<>
(shui/dropdown-menu-item
{:key "open"
:on-click #(route-handler/redirect-to-page! (:block/uuid value))}
(str "Open " (:block/title value)))
(shui/dropdown-menu-item
{:key "open sidebar"
:on-click #(state/sidebar-add-block! (state/get-current-repo) (:db/id value) :page)}
"Open in sidebar")])
{:as-dropdown? true
:content-props {:on-click (fn [] (shui/popup-hide!))}
:align "start"}))}]
(rum/with-key (page-cp opts value) (:db/id value))))
(contains? #{:node :class :property :page} type)
(when-let [reference (state/get-component :block/reference)]
(when value (reference {:table-view? table-view?} (:block/uuid value))))
(and (map? value) (some? (db-property/property-value-content value)))
(let [content (str (db-property/property-value-content value))]
(inline-text-cp content))
:else
(inline-text-cp (str value)))]))
(rum/defc single-value-select
[block property value select-opts {:keys [value-render] :as opts}]
(let [*el (hooks/use-ref nil)
editing? (:editing? opts)
type (:logseq.property/type property)
select-opts' (assoc select-opts :multiple-choices? false)
popup-content (fn content-fn [target]
[:div.property-select
(case type
(:entity :number :default :url :checkbox)
(select block property select-opts' opts)
(:node :class :property :page :date)
(property-value-select-node block property select-opts' (assoc opts :target target)))])
trigger-id (str "trigger-" (:container-id opts) "-" (:db/id block) "-" (:db/id property))
show-popup! (fn [target]
(shui/popup-show! target (fn [] (popup-content target))
{:align "start"
:as-dropdown? true
:auto-focus? true
:trigger-id trigger-id}))]
(if editing?
(popup-content nil)
(let [show! (fn [e]
(util/stop e)
(state/clear-selection!)
(let [target (when e (.-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")))))
(show-popup! target))))]
(shui/trigger-as
(if (:other-position? opts) :div.jtrigger :div.jtrigger.flex.flex-1.w-full.cursor-pointer)
{:ref *el
:id trigger-id
:tabIndex 0
:on-click show!
:on-key-down (fn [e]
(case (util/ekey e)
("Backspace" "Delete")
(delete-block-property! block property)
(" " "Enter")
(do (some-> (rum/deref *el) (.click))
(util/stop e))
nil))}
(if (string/blank? value)
(property-empty-text-value property opts)
(value-render)))))))
(defn- property-value-inner
[block property value {:keys [inline-text page-cp
dom-id row?]
:as opts}]
(let [multiple-values? (db-property/many? property)
class (str (when-not row? "flex flex-1 ")
(when multiple-values? "property-value-content"))
type (:logseq.property/type property)
text-ref-type? (db-property-type/text-ref-property-types type)]
[:div.cursor-text
{:id (or dom-id (random-uuid))
:tabIndex 0
:class (str class " " (when-not text-ref-type? "jtrigger"))
:on-key-down (fn [e]
(when-not text-ref-type?
(when (contains? #{"Backspace" "Delete"} (util/ekey e))
(delete-block-property! block property))))
:style {:min-height 24}}
(cond
(and (= :logseq.property/default-value (:db/ident property)) (nil? (:block/title value)))
[:div.jtrigger.cursor-pointer.text-sm.px-2
{:on-click #(<create-new-block! block property "")}
"Set default value"]
(= (:db/ident property) :logseq.property.publish/published-url)
[:div.flex.items-center.gap-2.w-full
[:a {:href (:block/title value)
:target "_blank"}
(:block/title value)]
(when-not config/publishing?
(shui/button
{:variant :text
:size :sm
:class "text-xs"
:on-click (fn [e]
(util/stop e)
(publish-handler/unpublish-page! block))}
"Unpublish"))]
text-ref-type?
(property-block-value value block property page-cp opts)
:else
(let [content (inline-text {} :markdown (macro-util/expand-value-if-macro (str value) (state/get-macros)))]
(cond
(contains? (set (keys string-value-on-click))
(:db/ident property))
[:div.w-full {:on-click (fn []
(let [f (get string-value-on-click (:db/ident property))]
(f block property)))}
content]
:else
content)))]))
(rum/defc single-number-input
[block property value-block table-view?]
(let [[editing? set-editing!] (hooks/use-state false)
*ref (hooks/use-ref nil)
*input-ref (hooks/use-ref nil)
number-value (db-property/property-value-content value-block)
[value set-value!] (hooks/use-state number-value)
[*value _] (hooks/use-state (atom value))
set-property-value! (fn [value & {:keys [exit-editing?]
:or {exit-editing? true}}]
(p/do!
(if (string/blank? value)
(when (get block (:db/ident property))
(db-property-handler/remove-block-property! (:db/id block) (:db/ident property)))
(when (not= (string/trim (str number-value))
(string/trim (str value)))
(db-property-handler/set-block-property! (:db/id block)
(:db/ident property)
value)))
(set-value! (str (db-property/property-value-content
(get (db/entity (:db/id block)) (:db/ident property)))))
(when exit-editing?
(set-editing! false))))]
(hooks/use-effect!
(fn []
#(set-property-value! @*value))
[])
(hooks/use-effect!
(fn []
(set-value! number-value)
#())
[number-value])
[:div.ls-number.flex.flex-1.jtrigger
{:ref *ref
:on-click #(do
(state/clear-selection!)
(set-editing! true))}
(if editing?
(shui/input
{:ref *input-ref
:auto-focus true
:class (str "ls-number-input h-6 px-0 py-0 border-none bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 text-base"
(when table-view? " text-sm"))
:value value
:type "number"
:on-change (fn [e]
(set-value! (util/evalue e))
(reset! *value (util/evalue e)))
:on-blur (fn [_e]
(p/do!
(set-property-value! value)))
:on-key-down (fn [e]
(let [input (rum/deref *input-ref)
pos (cursor/pos input)
k (util/ekey e)]
(when-not (util/input-text-selected? input)
(case k
("ArrowUp" "ArrowDown")
(do
(util/stop-propagation e)
(set-editing! false)
(editor-handler/move-cross-boundary-up-down (if (= "ArrowUp" (util/ekey e)) :up :down) {})
(set-property-value! value {:exit-editing? false}))
"Backspace"
(when (zero? pos)
(p/do!
(db-property-handler/remove-block-property! (:db/id block) (:db/ident property))
(editor-handler/move-cross-boundary-up-down :up {:pos :max})))
("Escape" "Enter")
(p/do!
(set-property-value! value)
(.focus (rum/deref *ref)))
nil))))})
value)]))
(rum/defcs property-scalar-value-aux < rum/static rum/reactive
[state block property value* {:keys [editing? on-chosen]
:as opts}]
(let [property (model/sub-block (:db/id property))
type (:logseq.property/type property)
batch? (batch-operation?)
closed-values? (seq (:property/closed-values property))
select-type?' (or (select-type? block property)
(and editing? batch? (contains? #{:default :url :checkbox} type) (not closed-values?)))
select-opts {:on-chosen on-chosen}
value (if (and (entity-map? value*) (= (:db/ident value*) :logseq.property/empty-placeholder))
nil
value*)]
(cond
(= :logseq.property/icon (:db/ident property))
(icon-row block editing?)
(and (= type :number) (not editing?) (not closed-values?))
(single-number-input block property value (:table-view? opts))
(= type :string)
(single-string-input block property value (:table-view? opts))
:else
(if (and select-type?'
(not (and (not closed-values?) (= type :date))))
(let [classes (outliner-property/get-block-classes (db/get-db) (:db/id block))
display-as-checkbox? (and (some
(fn [block]
(-> (set (map :db/id (:logseq.property/checkbox-display-properties block)))
(contains? (:db/id property))))
(conj classes block))
(seq (:property/closed-values property))
(boolean? (:logseq.property/choice-checkbox-state value*)))]
(if display-as-checkbox?
(let [checked? (:logseq.property/choice-checkbox-state value*)]
(shui/checkbox {:checked checked?
:class "mt-1"
:on-checked-change (fn [value]
(let [choices (:property/closed-values property)
choice (some (fn [choice] (when (= value (:logseq.property/choice-checkbox-state choice))
choice)) choices)]
(when choice
(db-property-handler/set-block-property! (:db/id block) (:db/ident property) (:db/id choice)))))}))
(single-value-select block property value
select-opts
(assoc opts
:editing? editing?
:value-render (fn [] (select-item property type value opts))))))
(case type
(:date :datetime)
(property-value-date-picker block property value (merge opts {:editing? editing?}))
:checkbox
(let [add-property! (fn [value]
(<add-property! block (:db/ident property) value opts)
(when-let [on-checked-change (:on-checked-change opts)]
(on-checked-change 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 (fn []
(add-property! (boolean (not value))))
:on-key-down (fn [e]
(when (= (util/ekey e) "Enter")
(add-property! (boolean (not value))))
(when (contains? #{"Backspace" "Delete"} (util/ekey e))
(delete-block-property! block property)))})])
;; :others
[:div.flex.flex-1
(property-value-inner block property value opts)])))))
(rum/defc property-scalar-value
[block property value* {:keys [container-id editing?]
:as opts}]
(let [block-editing? (state/sub-editing? [container-id (:block/uuid block)])
editing (or editing?
(and block-editing?
(= (:db/id property) (:db/id (:property (state/get-editor-action-data))))))]
(property-scalar-value-aux block property value* (assoc opts :editing? editing))))
(rum/defc multiple-values-inner
[block property v {:keys [on-chosen editing?] :as opts}]
(let [type (:logseq.property/type property)
date? (= type :date)
*el (hooks/use-ref nil)
items (cond->> (if (entity-map? v) #{v} v)
(= (:db/ident property) :block/tags)
(remove (fn [v] (contains? ldb/hidden-tags (:db/ident v)))))
select-cp (fn [select-opts target]
(let [select-opts (merge {:multiple-choices? true
:on-chosen (fn []
(when on-chosen (on-chosen)))}
select-opts
(when-not editing?
{:dropdown? false}))]
[:div.property-select
(if (contains? #{:node :page :class :property} type)
(property-value-select-node block property
(assoc select-opts :target target)
opts)
(select block property select-opts opts))]))]
(if editing?
(select-cp {} nil)
(let [toggle-fn shui/popup-hide!
content-fn (fn [{:keys [_id content-props]} target]
(select-cp {:content-props content-props} target))
show-popup! (fn [^js e]
(let [target (.-target e)]
(when-not (or (util/link? target) (.closest target "a") config/publishing?)
(shui/popup-show! (rum/deref *el)
(fn [opts]
(content-fn opts target))
{:as-dropdown? true :as-content? false
:align "start" :auto-focus? true}))))]
[:div.multi-values.jtrigger
{:tab-index "0"
:ref *el
:on-click show-popup!
:on-key-down (fn [^js e]
(case (.-key e)
(" " "Enter")
(do (some-> (rum/deref *el) (.click))
(util/stop e))
("Backspace" "Delete")
(delete-block-property! block property)
:dune))
:class "flex flex-1 flex-row items-center flex-wrap gap-1"}
(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 (assoc opts :show-popup! show-popup!))
(or (:block/uuid item) (str item))))
(interpose [:span.opacity-50.-ml-1 ","]))
(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 property opts))))]))))
(rum/defc multiple-values < rum/reactive db-mixins/query
[block property opts]
(let [value (get block (:db/ident property))
value' (if (coll? value) value
(when (some? value) #{value}))]
(multiple-values-inner block property value' opts)))
(rum/defcs ^:large-vars/cleanup-todo property-value < rum/reactive db-mixins/query
[state block property {:keys [show-tooltip? p-block p-property editing?]
:as opts}]
(ui/catch-error
(ui/block-error "Something wrong" {})
(let [block-cp (state/get-component :block/blocks-container)
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")
type (:logseq.property/type property)
multiple-values? (db-property/many? property)
v (let [v (get block (:db/ident property))]
(or
(cond
(and multiple-values? (or (set? v) (coll? v) (nil? v)))
v
multiple-values?
#{v}
(set? v)
(first v)
:else
v)
(:logseq.property/default-value property)))
self-value-or-embedded? (fn [v]
(or (= (:db/id v) (:db/id block))
;; property value self embedded
(and (:db/id block) (= (:db/id (:block/link v)) (:db/id block)))))]
(if (and (or (and (entity-map? v)
(self-value-or-embedded? v))
(and (coll? v) (every? entity-map? v)
(some self-value-or-embedded? v))
(and (:db/id block)
(= p-block (:db/id block))
(= p-property (:db/id property))))
(not= :logseq.class/Tag
(:db/ident (db/entity (:db/id block)))))
[:div.flex.flex-row.items-center.gap-1
[:div.warning "Self reference"]
(shui/button {:variant :outline
:size :sm
:class "h-5"
:on-click (fn []
(db-property-handler/remove-block-property!
(:db/id block)
(:db/ident property)))}
"Fix it!")]
(let [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"))
:on-pointer-down (fn [e]
(when-not (some-> (.-target e) (.closest "[data-radix-popper-content-wrapper]"))
(state/clear-selection!)))}
(cond
(and multiple-values? (contains? #{:default :url} type) (not closed-values?) (not editing?))
(property-normal-block-value block property v opts)
multiple-values?
(multiple-values block property opts)
:else
(property-scalar-value block property v
(merge
opts
{:editor-id editor-id
:dom-id dom-id})))]]
(if show-tooltip?
(shui/tooltip-provider
(shui/tooltip
{:delayDuration 1200}
(shui/tooltip-trigger
{:onFocusCapture #(util/stop-propagation %)
:as-child true}
value-cp)
(shui/tooltip-content
(str "Change " (:block/title property)))))
value-cp))))))