value.cljs 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105
  1. (ns frontend.components.property.value
  2. (:require [cljs-time.coerce :as tc]
  3. [clojure.string :as string]
  4. [datascript.impl.entity :as de]
  5. [dommy.core :as d]
  6. [frontend.components.icon :as icon-component]
  7. [frontend.components.query.builder :as query-builder-component]
  8. [frontend.components.select :as select]
  9. [frontend.components.title :as title]
  10. [frontend.config :as config]
  11. [frontend.date :as date]
  12. [frontend.db :as db]
  13. [frontend.db-mixins :as db-mixins]
  14. [frontend.db.async :as db-async]
  15. [frontend.db.model :as model]
  16. [frontend.handler.block :as block-handler]
  17. [frontend.handler.db-based.page :as db-page-handler]
  18. [frontend.handler.db-based.property :as db-property-handler]
  19. [frontend.handler.editor :as editor-handler]
  20. [frontend.handler.page :as page-handler]
  21. [frontend.handler.property :as property-handler]
  22. [frontend.handler.property.util :as pu]
  23. [frontend.handler.route :as route-handler]
  24. [frontend.modules.outliner.ui :as ui-outliner-tx]
  25. [frontend.search :as search]
  26. [frontend.state :as state]
  27. [frontend.ui :as ui]
  28. [frontend.util :as util]
  29. [goog.dom :as gdom]
  30. [goog.functions :refer [debounce]]
  31. [lambdaisland.glogi :as log]
  32. [logseq.common.util :as common-util]
  33. [logseq.common.util.macro :as macro-util]
  34. [logseq.db :as ldb]
  35. [logseq.db.frontend.property :as db-property]
  36. [logseq.db.frontend.property.type :as db-property-type]
  37. [logseq.shui.ui :as shui]
  38. [promesa.core :as p]
  39. [rum.core :as rum]))
  40. (rum/defc property-empty-btn-value
  41. [property & opts]
  42. (let [text (cond
  43. (= (:db/ident property) :logseq.property/description)
  44. "Add description"
  45. :else
  46. "Empty")]
  47. (if (= text "Empty")
  48. (shui/button (merge {:class "empty-btn" :variant :text} opts)
  49. text)
  50. (shui/button (merge {:class "empty-btn !text-base" :variant :text} opts)
  51. text))))
  52. (rum/defc property-empty-text-value
  53. [& {:as opts}]
  54. [:span.inline-flex.items-center.cursor-pointer
  55. (merge {:class "empty-text-btn" :variant :text} opts) "Empty"])
  56. (rum/defc icon-row
  57. [block editing?]
  58. (let [icon-value (:logseq.property/icon block)
  59. clear-overlay! (fn []
  60. (shui/dialog-close!)
  61. (shui/popup-hide-all!))
  62. on-chosen! (fn [_e icon]
  63. (if icon
  64. (db-property-handler/set-block-property!
  65. (:db/id block)
  66. :logseq.property/icon
  67. (select-keys icon [:type :id :color]))
  68. (db-property-handler/remove-block-property!
  69. (:db/id block)
  70. :logseq.property/icon))
  71. (clear-overlay!))]
  72. (rum/use-effect!
  73. (fn []
  74. (when editing?
  75. (clear-overlay!)
  76. (let [^js container (or (some-> js/document.activeElement (.closest ".page"))
  77. (gdom/getElement "main-content-container"))
  78. icon (get block (pu/get-pid :logseq.property/icon))]
  79. (util/schedule
  80. (fn []
  81. (when-let [^js target (some-> (.querySelector container (str "#ls-block-" (str (:block/uuid block))))
  82. (.querySelector ".block-main-container"))]
  83. (shui/popup-show! target
  84. #(icon-component/icon-search
  85. {:on-chosen on-chosen!
  86. :icon-value icon
  87. :del-btn? (some? icon)})
  88. {:id :ls-icon-picker
  89. :align :start})))))))
  90. [editing?])
  91. [:div.col-span-3.flex.flex-row.items-center.gap-2
  92. (icon-component/icon-picker icon-value
  93. {:disabled? config/publishing?
  94. :del-btn? (some? icon-value)
  95. :on-chosen on-chosen!})]))
  96. (defn- select-type?
  97. [property type]
  98. (or (contains? #{:node :number :url :date :page :class :property} type)
  99. ;; closed values
  100. (seq (:property/closed-values property))))
  101. (defn <create-new-block!
  102. [block property value & {:keys [edit-block?]
  103. :or {edit-block? true}}]
  104. (shui/popup-hide!)
  105. (shui/dialog-close!)
  106. (p/let [block
  107. (if (and (= :default (get-in property [:block/schema :type]))
  108. (not (db-property/many? property)))
  109. (p/let [existing-value (get block (:db/ident property))
  110. existing-value? (and (some? existing-value)
  111. (not= (:db/ident existing-value) :logseq.property/empty-placeholder))
  112. new-block-id (when-not existing-value? (db/new-block-id))
  113. _ (when-not existing-value?
  114. (db-property-handler/create-property-text-block!
  115. (:db/id block)
  116. (:db/id property)
  117. value
  118. {:new-block-id new-block-id}))]
  119. (if existing-value? existing-value (db/entity [:block/uuid new-block-id])))
  120. (p/let [new-block-id (db/new-block-id)
  121. _ (db-property-handler/create-property-text-block!
  122. (:db/id block)
  123. (:db/id property)
  124. value
  125. {:new-block-id new-block-id})]
  126. (db/entity [:block/uuid new-block-id])))]
  127. (when edit-block?
  128. (editor-handler/edit-block! block :max {:container-id :unknown-container}))
  129. block))
  130. (defn- get-operating-blocks
  131. [block]
  132. (let [selected-blocks (some->> (state/get-selection-block-ids)
  133. (map (fn [id] (db/entity [:block/uuid id])))
  134. (seq)
  135. block-handler/get-top-level-blocks
  136. (remove ldb/property?))]
  137. (or (seq selected-blocks) [block])))
  138. (defn <add-property!
  139. "If a class and in a class schema context, add the property to its schema.
  140. Otherwise, add a block's property and its value"
  141. ([block property-key property-value] (<add-property! block property-key property-value {}))
  142. ([block property-id property-value' {:keys [exit-edit? class-schema?]
  143. :or {exit-edit? true}}]
  144. (let [repo (state/get-current-repo)
  145. class? (ldb/class? block)
  146. property (db/entity property-id)
  147. many? (db-property/many? property)
  148. checkbox? (= :checkbox (get-in property [:block/schema :type]))
  149. blocks (get-operating-blocks block)]
  150. (assert (qualified-keyword? property-id) "property to add must be a keyword")
  151. (p/do!
  152. (if (and class? class-schema?)
  153. (db-property-handler/class-add-property! (:db/id block) property-id)
  154. (let [block-ids (map :block/uuid blocks)]
  155. (if (and (db-property-type/user-ref-property-types (get-in property [:block/schema :type]))
  156. (string? property-value'))
  157. (p/let [new-block (<create-new-block! block (db/entity property-id) property-value' {:edit-block? false})]
  158. (when (seq (remove #{(:db/id block)} (map :db/id block)))
  159. (property-handler/batch-set-block-property! repo block-ids property-id (:db/id new-block)))
  160. new-block)
  161. (property-handler/batch-set-block-property! repo block-ids property-id property-value'))))
  162. (when exit-edit?
  163. (ui/hide-popups-until-preview-popup!)
  164. (shui/dialog-close!))
  165. (when-not (or many? checkbox?)
  166. (when-let [input (state/get-input)]
  167. (.focus input)))
  168. (when checkbox?
  169. (state/set-editor-action-data! {:type :focus-property-value
  170. :property property}))))))
  171. (defn- add-or-remove-property-value
  172. [block property value selected? {:keys [refresh-result-f]}]
  173. (let [many? (db-property/many? property)
  174. blocks (get-operating-blocks block)]
  175. (p/do!
  176. (if selected?
  177. (<add-property! block (:db/ident property) value {:exit-edit? (not many?)})
  178. (p/do!
  179. (ui-outliner-tx/transact!
  180. {:outliner-op :save-block}
  181. (doseq [block blocks]
  182. (db-property-handler/delete-property-value! (:db/id block) (:db/ident property) value)))
  183. (when (or (not many?)
  184. ;; values will be cleared
  185. (and many? (<= (count (get block (:db/ident property))) 1)))
  186. (shui/popup-hide!))))
  187. (when (fn? refresh-result-f) (refresh-result-f)))))
  188. (rum/defcs calendar-inner <
  189. (rum/local (str "calendar-inner-" (js/Date.now)) ::identity)
  190. {:init (fn [state]
  191. (state/set-editor-action! :property-set-date)
  192. state)
  193. :will-mount (fn [state]
  194. (js/setTimeout
  195. #(some-> @(::identity state)
  196. (js/document.getElementById)
  197. (.querySelector "[aria-selected=true]")
  198. (.focus)) 16)
  199. state)
  200. :will-unmount (fn [state]
  201. (shui/popup-hide!)
  202. (shui/dialog-close!)
  203. (state/set-editor-action! nil)
  204. state)}
  205. [state id {:keys [datetime? on-change value del-btn? on-delete]}]
  206. (let [*ident (::identity state)
  207. initial-day (or (some-> value (.getTime) (js/Date.)) (js/Date.))
  208. initial-month (when value
  209. (js/Date. (.getFullYear value) (.getMonth value)))
  210. select-handler!
  211. (fn [^js d]
  212. ;; force local to UTC
  213. (when d
  214. (let [gd (goog.date.Date. (.getFullYear d) (.getMonth d) (.getDate d))]
  215. (let [journal (date/js-date->journal-title gd)]
  216. (p/do!
  217. (when-not (db/get-page journal)
  218. (page-handler/<create! journal {:redirect? false
  219. :create-first-block? false}))
  220. (when (fn? on-change)
  221. (let [value (if datetime? (tc/to-long d) (db/get-page journal))]
  222. (on-change value)))
  223. (shui/popup-hide! id)
  224. (ui/hide-popups-until-preview-popup!)
  225. (shui/dialog-close!))))))]
  226. (ui/nlp-calendar
  227. (cond->
  228. {:initial-focus true
  229. :datetime? datetime?
  230. :selected initial-day
  231. :id @*ident
  232. :del-btn? del-btn?
  233. :on-delete on-delete
  234. :on-select select-handler!}
  235. initial-month
  236. (assoc :default-month initial-month)))))
  237. (rum/defc date-picker
  238. [value {:keys [datetime? on-change on-delete del-btn? editing? multiple-values? other-position?]}]
  239. (let [*trigger-ref (rum/use-ref nil)
  240. value' (cond
  241. (map? value)
  242. (js/Date. (date/journal-title->long (:block/title value)))
  243. (number? value)
  244. (js/Date. value)
  245. :else
  246. (let [d (js/Date.)]
  247. (.setHours d 0 0 0)
  248. d))
  249. content-fn (fn [{:keys [id]}] (calendar-inner id
  250. {:on-change on-change
  251. :value value'
  252. :del-btn? del-btn?
  253. :on-delete on-delete
  254. :datetime? datetime?}))
  255. open-popup! (fn [e]
  256. (when-not (or (util/meta-key? e) (util/shift-key? e))
  257. (util/stop e)
  258. (when-not config/publishing?
  259. (shui/popup-show! (.-target e) content-fn
  260. {:align "start" :auto-focus? true}))))]
  261. (rum/use-effect!
  262. (fn []
  263. (when editing?
  264. (js/setTimeout
  265. #(some-> (rum/deref *trigger-ref)
  266. (.click)) 32)))
  267. [editing?])
  268. (if multiple-values?
  269. (shui/button
  270. {:class "jtrigger h-6 empty-btn"
  271. :ref *trigger-ref
  272. :variant :text
  273. :size :sm
  274. :on-click open-popup!}
  275. (ui/icon "calendar-plus" {:size 16}))
  276. (shui/trigger-as
  277. :div.flex.flex-1.flex-row.gap-1.items-center.flex-wrap
  278. {:tabIndex 0
  279. :class "jtrigger min-h-[24px]" ; FIXME: min-h-6 not works
  280. :ref *trigger-ref
  281. :on-click open-popup!}
  282. (cond
  283. (map? value)
  284. (when-let [page-cp (state/get-component :block/page-cp)]
  285. (rum/with-key
  286. (page-cp {:disable-preview? true
  287. :meta-click? other-position?} value)
  288. (:db/id value)))
  289. (number? value)
  290. (when-let [date (js/Date. value)]
  291. [:div.flex.flex-row.gap-1.items-center
  292. (when-let [page-cp (state/get-component :block/page-cp)]
  293. (let [page-title (date/journal-name (date/js-date->goog-date date))]
  294. (page-cp {:disable-preview? true
  295. :show-non-exists-page? true}
  296. {:block/name page-title})))
  297. [:span.opacity-50
  298. (str (util/zero-pad (.getHours date))
  299. ":"
  300. (util/zero-pad (.getMinutes date)))]])
  301. :else
  302. (property-empty-btn-value nil))))))
  303. (rum/defc property-value-date-picker
  304. [block property value opts]
  305. (let [multiple-values? (db-property/many? property)
  306. repo (state/get-current-repo)
  307. datetime? (= :datetime (get-in property [:block/schema :type]))]
  308. (date-picker value
  309. (merge opts
  310. {:datetime? datetime?
  311. :multiple-values? multiple-values?
  312. :on-change (fn [value]
  313. (property-handler/set-block-property! repo (:block/uuid block)
  314. (:db/ident property)
  315. (if (map? value) (:db/id value) value)))
  316. :del-btn? (some-> value (:block/title) (boolean))
  317. :on-delete (fn []
  318. (property-handler/set-block-property! repo (:block/uuid block)
  319. (:db/ident property) nil)
  320. (shui/popup-hide!))}))))
  321. (defn- <create-page-if-not-exists!
  322. [block property classes page]
  323. (let [page* (string/trim page)
  324. ;; inline-class is only for input from :transform-fn
  325. [page inline-class] (if (and (seq classes) (not (contains? db-property/db-attribute-properties (:db/ident property))))
  326. (or (seq (map string/trim (rest (re-find #"(.*)#(.*)$" page*))))
  327. [page* nil])
  328. [page* nil])
  329. page-entity (ldb/get-case-page (db/get-db) page)
  330. id (:db/id page-entity)
  331. class? (or (= :block/tags (:db/ident property))
  332. (and (= :logseq.property/parent (:db/ident property))
  333. (ldb/class? block)))
  334. ;; Note: property and other types shouldn't be converted to class
  335. page? (= "page" (:block/type page-entity))]
  336. (cond
  337. ;; page not exists or page exists but not a page type
  338. (or (nil? id) (and class? (not page?)))
  339. (let [inline-class-uuid
  340. (when inline-class
  341. (or (:block/uuid (ldb/get-case-page (db/get-db) inline-class))
  342. (do (log/error :msg "Given inline class does not exist" :inline-class inline-class)
  343. nil)))
  344. create-options {:redirect? false
  345. :create-first-block? false
  346. :tags (if inline-class-uuid
  347. [inline-class-uuid]
  348. ;; Only 1st class b/c page normally has
  349. ;; one of and not all these classes
  350. (mapv :block/uuid (take 1 classes)))}]
  351. (p/let [page (if class?
  352. (db-page-handler/<create-class! page create-options)
  353. (page-handler/<create! page create-options))]
  354. (:db/id page)))
  355. (and class? page? id)
  356. (p/let [_ (db-page-handler/convert-to-tag! page-entity)]
  357. id)
  358. :else
  359. id)))
  360. (defn- select-aux
  361. [block property {:keys [items selected-choices multiple-choices?] :as opts}]
  362. (let [selected-choices (->> selected-choices
  363. (remove nil?)
  364. (remove #(= :logseq.property/empty-placeholder %)))
  365. clear-value (str "No " (:block/title property))
  366. clear-value-label [:div.flex.flex-row.items-center.gap-2
  367. (ui/icon "x")
  368. [:div clear-value]]
  369. items' (->>
  370. (if (and (seq selected-choices)
  371. (not multiple-choices?)
  372. (not (and (ldb/class? block) (= (:db/ident property) :logseq.property/parent)))
  373. (not= (:db/ident property) :logseq.property.view/type))
  374. (concat items
  375. [{:value clear-value
  376. :label clear-value-label
  377. :clear? true}])
  378. items)
  379. (remove #(= :logseq.property/empty-placeholder (:value %))))
  380. k :on-chosen
  381. f (get opts k)
  382. f' (fn [chosen selected?]
  383. (if (or (and (not multiple-choices?) (= chosen clear-value))
  384. (and multiple-choices? (= chosen [clear-value])))
  385. (p/do!
  386. (let [blocks (get-operating-blocks block)
  387. block-ids (map :block/uuid blocks)]
  388. (property-handler/batch-remove-block-property!
  389. (state/get-current-repo)
  390. block-ids
  391. (:db/ident property)))
  392. (shui/popup-hide!))
  393. (f chosen selected?)))]
  394. (select/select (assoc opts
  395. :selected-choices selected-choices
  396. :items items'
  397. k f'))
  398. ;(shui/multi-select-content
  399. ; (map #(let [{:keys [value label]} %]
  400. ; {:id value :value label}) items') nil opts)
  401. ))
  402. (defn- get-node-icon
  403. [node]
  404. (cond
  405. (ldb/class? node)
  406. "hash"
  407. (ldb/property? node)
  408. "letter-p"
  409. (ldb/page? node)
  410. "page"
  411. :else
  412. "letter-n"))
  413. (rum/defcs ^:large-vars/cleanup-todo select-node < rum/reactive db-mixins/query
  414. (rum/local 0 ::refresh-count)
  415. [state property
  416. {:keys [block multiple-choices? dropdown? input-opts on-input] :as opts}
  417. *result]
  418. (let [*refresh-count (::refresh-count state)
  419. ;; Trigger refresh
  420. _ @*refresh-count
  421. repo (state/get-current-repo)
  422. classes (:property/schema.classes property)
  423. tags? (= :block/tags (:db/ident property))
  424. alias? (= :block/alias (:db/ident property))
  425. tags-or-alias? (or tags? alias?)
  426. block (db/entity (:db/id block))
  427. result (rum/react *result)
  428. selected-choices (when block
  429. (when-let [v (get block (:db/ident property))]
  430. (if (every? de/entity? v)
  431. (map :db/id v)
  432. [(:db/id v)])))
  433. parent-property? (= (:db/ident property) :logseq.property/parent)
  434. children-pages (when parent-property? (model/get-structured-children repo (:db/id block)))
  435. nodes
  436. (->>
  437. (cond
  438. parent-property?
  439. (let [;; Disallows cyclic hierarchies
  440. exclude-ids (-> (set (map (fn [id] (:block/uuid (db/entity id))) children-pages))
  441. (conj (:block/uuid block))) ; break cycle
  442. options (if (ldb/class? block)
  443. (model/get-all-classes repo)
  444. (cond->>
  445. (->> (model/get-all-pages repo)
  446. (remove (fn [e] (or (ldb/built-in? e) (ldb/property? e)))))
  447. (contains? #{"property" "page"} (:block/type block))
  448. (remove ldb/class?)))
  449. excluded-options (remove (fn [e] (contains? exclude-ids (:block/uuid e))) options)]
  450. excluded-options)
  451. (seq classes)
  452. (mapcat
  453. (fn [class]
  454. (if (= :logseq.class/Root (:db/ident class))
  455. (model/get-all-classes repo {:except-root-class? true})
  456. (model/get-class-objects repo (:db/id class))))
  457. classes)
  458. :else
  459. (let [property-type (get-in property [:block/schema :type])]
  460. (if (empty? result)
  461. (let [v (get block (:db/ident property))]
  462. (remove #(= :logseq.property/empty-placeholder (:db/ident %))
  463. (if (every? de/entity? v) v [v])))
  464. (remove (fn [node]
  465. (or (= (:db/id block) (:db/id node))
  466. ;; A page's alias can't be itself
  467. (and alias? (= (or (:db/id (:block/page block))
  468. (:db/id block))
  469. (:db/id node)))
  470. (when (and property-type (not= property-type :node))
  471. (if (= property-type :page)
  472. (not (db/page? node))
  473. (not= property-type (some-> (:block/type node) keyword))))))
  474. result)))))
  475. options (map (fn [node]
  476. (let [id (or (:value node) (:db/id node))
  477. label (if (integer? id)
  478. (let [title (subs (title/block-unique-title node) 0 256)
  479. node (or (db/entity id) node)
  480. icon (get-node-icon node)]
  481. [:div.flex.flex-col
  482. (when-not (db/page? node)
  483. (when-let [breadcrumb (state/get-component :block/breadcrumb)]
  484. [:div.text-xs.opacity-70.mb-1 {:style {:margin-left 3}}
  485. (breadcrumb {:search? true} (state/get-current-repo) (:block/uuid block) {})]))
  486. [:div.flex.flex-row.items-center.gap-1
  487. (when-not (:property/schema.classes property)
  488. (ui/icon icon {:size 14}))
  489. [:div title]]])
  490. (or (:label node) (:block/title node)))]
  491. (assoc node
  492. :label-value (:block/title node)
  493. :label label
  494. :value id))) nodes)
  495. classes' (remove (fn [class] (= :logseq.class/Root (:db/ident class))) classes)
  496. opts' (cond->
  497. (merge
  498. opts
  499. {:multiple-choices? multiple-choices?
  500. :items options
  501. :selected-choices selected-choices
  502. :dropdown? dropdown?
  503. :input-default-placeholder (cond
  504. tags?
  505. "Set tags"
  506. alias?
  507. "Set alias"
  508. multiple-choices?
  509. "Choose nodes"
  510. :else
  511. "Choose node")
  512. :show-new-when-not-exact-match? (if (and parent-property? (contains? (set children-pages) (:db/id block)))
  513. false
  514. true)
  515. :extract-chosen-fn :value
  516. :extract-fn (fn [x] (or (:label-value x) (:label x)))
  517. :input-opts input-opts
  518. :on-input (debounce on-input 50)
  519. :on-chosen (fn [chosen selected?]
  520. (p/let [[id new?] (if (integer? chosen)
  521. [chosen false]
  522. (when-not (string/blank? (string/trim chosen))
  523. (p/let [result (<create-page-if-not-exists! block property classes' chosen)]
  524. [result true])))
  525. _ (when (and (integer? id) (not (ldb/page? (db/entity id))))
  526. (db-async/<get-block repo id))]
  527. (p/do!
  528. (if id
  529. (add-or-remove-property-value block property id selected? {})
  530. (log/error :msg "No :db/id found or created for chosen" :chosen chosen))
  531. (when new? (swap! *refresh-count inc)))))})
  532. (and (seq classes') (not tags-or-alias?))
  533. (assoc
  534. ;; Provides additional completion for inline classes on new pages or objects
  535. :transform-fn (fn [results input]
  536. (if-let [[_ new-page class-input] (and (empty? results) (re-find #"(.*)#(.*)$" input))]
  537. (let [repo (state/get-current-repo)
  538. descendent-classes (->> classes'
  539. (mapcat #(model/get-structured-children repo (:db/id %)))
  540. (map #(db/entity repo %)))]
  541. (->> (concat classes' descendent-classes)
  542. (filter #(string/includes? (:block/title %) class-input))
  543. (mapv (fn [p]
  544. {:value (str new-page "#" (:block/title p))
  545. :label (str new-page "#" (:block/title p))}))))
  546. results))))]
  547. (select-aux block property opts')))
  548. (rum/defcs property-value-select-node <
  549. (rum/local nil ::result)
  550. [state block property opts
  551. {:keys [*show-new-property-config?]}]
  552. (let [*result (::result state)
  553. input-opts (fn [_]
  554. {:on-click (fn []
  555. (when *show-new-property-config?
  556. (reset! *show-new-property-config? false)))
  557. :on-key-down
  558. (fn [e]
  559. (case (util/ekey e)
  560. "Escape"
  561. (when-let [f (:on-chosen opts)] (f))
  562. nil))})
  563. opts' (assoc opts
  564. :block block
  565. :input-opts input-opts
  566. :on-input (fn [v]
  567. (if (string/blank? v)
  568. (reset! *result nil)
  569. (p/let [result (search/block-search (state/get-current-repo) v {:enable-snippet? false
  570. :built-in? false})]
  571. (reset! *result result)))))]
  572. (select-node property opts' *result)))
  573. (rum/defcs select < rum/reactive db-mixins/query
  574. {:init (fn [state]
  575. (let [*values (atom :loading)
  576. refresh-result-f (fn []
  577. (p/let [result (db-async/<get-block-property-values (state/get-current-repo)
  578. (:db/ident (nth (:rum/args state) 1)))]
  579. (reset! *values result)))]
  580. (refresh-result-f)
  581. (assoc state
  582. ::values *values
  583. ::refresh-result-f refresh-result-f)))}
  584. [state block property
  585. {:keys [multiple-choices? dropdown? content-props] :as select-opts}
  586. {:keys [*show-new-property-config?]}]
  587. (let [*values (::values state)
  588. refresh-result-f (::refresh-result-f state)
  589. values (rum/react *values)
  590. block (db/sub-block (:db/id block))]
  591. (when-not (= :loading values)
  592. (let [schema (:block/schema property)
  593. type (:type schema)
  594. closed-values? (seq (:property/closed-values property))
  595. ref-type? (db-property-type/user-ref-property-types type)
  596. items (if closed-values?
  597. (keep (fn [block]
  598. (let [icon (pu/get-block-property-value block :logseq.property/icon)
  599. value (db-property/closed-value-content block)]
  600. {:label (if icon
  601. [:div.flex.flex-row.gap-1.items-center
  602. (icon-component/icon icon {:color? true})
  603. value]
  604. value)
  605. :value (:db/id block)
  606. :label-value value})) (:property/closed-values property))
  607. (->> values
  608. (mapcat (fn [value]
  609. (if (coll? value)
  610. (map (fn [v] {:value v}) value)
  611. [{:value value}])))
  612. (map (fn [{:keys [value]}]
  613. (if (and ref-type? (number? value))
  614. (when-let [e (db/entity value)]
  615. {:label (db-property/property-value-content e)
  616. :value value})
  617. {:label value
  618. :value value})))
  619. (distinct)))
  620. items (->> (if (= :date type)
  621. (map (fn [m] (let [label (:block/title (db/entity (:value m)))]
  622. (when label
  623. (assoc m :label label)))) items)
  624. items)
  625. (remove nil?))
  626. on-chosen (fn [chosen selected?]
  627. (let [value (if (map? chosen) (:value chosen) chosen)]
  628. (add-or-remove-property-value block property value selected?
  629. {:refresh-result-f refresh-result-f})))
  630. selected-choices' (get block (:db/ident property))
  631. selected-choices (if (every? de/entity? selected-choices')
  632. (map :db/id selected-choices')
  633. [selected-choices'])]
  634. (select-aux block property
  635. {:multiple-choices? multiple-choices?
  636. :items items
  637. :selected-choices selected-choices
  638. :dropdown? dropdown?
  639. :show-new-when-not-exact-match? (not (or closed-values? (= :date type)))
  640. :input-default-placeholder "Select"
  641. :extract-chosen-fn :value
  642. :extract-fn (fn [x] (or (:label-value x) (:label x)))
  643. :content-props content-props
  644. :on-chosen on-chosen
  645. :input-opts (fn [_]
  646. {:on-blur (fn []
  647. (when-let [f (:on-chosen select-opts)] (f)))
  648. :on-click (fn []
  649. (when *show-new-property-config?
  650. (reset! *show-new-property-config? false)))
  651. :on-key-down
  652. (fn [e]
  653. (case (util/ekey e)
  654. "Escape"
  655. (when-let [f (:on-chosen select-opts)] (f))
  656. nil))})})))))
  657. (rum/defcs property-normal-block-value <
  658. {:init (fn [state]
  659. (assoc state :container-id (state/get-next-container-id)))}
  660. [state block property value-block]
  661. (let [container-id (:container-id state)
  662. multiple-values? (db-property/many? property)
  663. block-container (state/get-component :block/container)
  664. blocks-container (state/get-component :block/blocks-container)
  665. value-block (if (and (coll? value-block) (every? de/entity? value-block))
  666. (set (remove #(= (:db/ident %) :logseq.property/empty-placeholder) value-block))
  667. value-block)]
  668. (if (seq value-block)
  669. [:div.property-block-container.content.w-full
  670. (let [config {:id (str (if multiple-values?
  671. (:block/uuid block)
  672. (:block/uuid value-block)))
  673. :container-id container-id
  674. :editor-box (state/get-component :editor/box)
  675. :property-block? true}]
  676. (if (set? value-block)
  677. (blocks-container config (ldb/sort-by-order value-block))
  678. (rum/with-key
  679. (block-container config value-block)
  680. (str (:db/id property) "-" (:block/uuid value-block)))))]
  681. [:div
  682. {:tabIndex 0
  683. :on-click (fn [] (<create-new-block! block property ""))}
  684. (property-empty-btn-value property)])))
  685. (rum/defcs query-cp <
  686. (rum/local false ::show-setting?)
  687. [state block property v-block]
  688. (let [result (common-util/safe-read-string (:block/title v-block))
  689. advanced-query? (or (and (map? result) (:query result))
  690. (string/starts-with? (string/triml (:block/title v-block)) "{"))]
  691. [:div.flex.flex-1.flex-row.gap-1.justify-between
  692. [:div.flex.flex-1 (property-normal-block-value block property v-block)]
  693. (when-not advanced-query?
  694. (shui/button
  695. {:variant :ghost
  696. :size :sm
  697. :class "jtrigger px-1 text-muted-foreground"
  698. :title "Update query"
  699. :on-click (fn [e]
  700. (shui/popup-show!
  701. (.-target e)
  702. (fn []
  703. [:div.p-4.h-64 {:style {:width "42rem"}}
  704. (let [block (db/entity (:db/id v-block))
  705. query (:block/title block)]
  706. (query-builder-component/builder query {:property property
  707. :block block}))])
  708. {:align :end}))}
  709. (ui/icon "settings" {:size 18})))]))
  710. (rum/defcs property-block-value < rum/reactive db-mixins/query
  711. {:init (fn [state]
  712. (let [block (first (:rum/args state))]
  713. (when-let [block-id (or (:db/id block) (:block/uuid block))]
  714. (db-async/<get-block (state/get-current-repo) block-id :children? true)))
  715. state)}
  716. [state value block property page-cp]
  717. (when value
  718. (if (state/sub-async-query-loading value)
  719. [:div.text-sm.opacity-70 "loading"]
  720. (if-let [v-block (db/sub-block (:db/id value))]
  721. (let [class? (ldb/class? v-block)
  722. invalid-warning [:div.warning.text-sm
  723. "Invalid block value, please delete the current property."]]
  724. (when v-block
  725. (cond
  726. (= (:db/ident property) :logseq.property/query)
  727. (query-cp block property v-block)
  728. (:block/page v-block)
  729. (property-normal-block-value block property v-block)
  730. ;; page/class/etc.
  731. (ldb/page? v-block)
  732. (rum/with-key
  733. (page-cp {:disable-preview? true
  734. :tag? class?} v-block)
  735. (:db/id v-block))
  736. :else
  737. invalid-warning)))
  738. (property-empty-btn-value property)))))
  739. (rum/defc closed-value-item < rum/reactive db-mixins/query
  740. [value {:keys [inline-text icon?]}]
  741. (when value
  742. (let [eid (if (de/entity? value) (:db/id value) [:block/uuid value])]
  743. (when-let [block (db/sub-block (:db/id (db/entity eid)))]
  744. (let [property-block? (db-property/property-created-block? block)
  745. value' (db-property/closed-value-content block)
  746. icon (pu/get-block-property-value block :logseq.property/icon)]
  747. (cond
  748. icon
  749. (if icon?
  750. (icon-component/icon icon {:color? true})
  751. [:div.flex.flex-row.items-center.gap-2.h-6
  752. (icon-component/icon icon {:color? true})
  753. (when value'
  754. [:span value'])])
  755. property-block?
  756. value'
  757. (= type :number)
  758. [:span.number (str value')]
  759. :else
  760. (inline-text {} :markdown (str value'))))))))
  761. (rum/defc select-item
  762. [property type value {:keys [page-cp inline-text other-position? _icon?] :as opts}]
  763. (let [closed-values? (seq (:property/closed-values property))
  764. tag? (or (:tag? opts) (= (:db/ident property) :block/tags))
  765. inline-text-cp (fn [content]
  766. [:div.flex.flex-row.items-center
  767. (inline-text {} :markdown (macro-util/expand-value-if-macro content (state/get-macros)))
  768. (when (and (= type :url) other-position?)
  769. (shui/button {:variant :ghost
  770. :size :sm
  771. :class "px-0 py-0 h-4"}
  772. (ui/icon "edit" {:size 14})))])]
  773. [:div.select-item.cursor-pointer
  774. (cond
  775. (= value :logseq.property/empty-placeholder)
  776. (property-empty-btn-value property)
  777. (or (ldb/page? value)
  778. (and (seq (:block/tags value))
  779. ;; FIXME: page-cp should be renamed to node-cp and
  780. ;; support this case and maybe other complex cases.
  781. (not (string/includes? (:block/title value) "[["))))
  782. (when value
  783. (rum/with-key
  784. (page-cp {:disable-preview? true
  785. :tag? tag?
  786. :meta-click? other-position?} value)
  787. (:db/id value)))
  788. (contains? #{:node :class :property :page} type)
  789. (when-let [reference (state/get-component :block/reference)]
  790. (reference {} (:block/uuid value)))
  791. closed-values?
  792. (closed-value-item value opts)
  793. (de/entity? value)
  794. (when-some [content (if (some? (:property.value/content value))
  795. ;; content needs to be a string for display purposes
  796. (str (:property.value/content value))
  797. (:block/title value))]
  798. (inline-text-cp content))
  799. :else
  800. (inline-text-cp (str value)))]))
  801. (rum/defc single-value-select
  802. [block property value value-f select-opts opts]
  803. (let [*el (rum/use-ref nil)]
  804. ;; Open popover initially when editing a property
  805. (rum/use-effect!
  806. (fn []
  807. (when (:editing? opts)
  808. (.click (rum/deref *el))))
  809. [(:editing? opts)])
  810. (let [schema (:block/schema property)
  811. type (get schema :type :default)
  812. select-opts' (assoc select-opts :multiple-choices? false)
  813. popup-content (fn content-fn [_]
  814. [:div.property-select
  815. (case type
  816. (:number :url :default)
  817. (select block property select-opts' opts)
  818. (:node :class :property :page :date)
  819. (property-value-select-node block property select-opts' opts))])
  820. trigger-id (str "trigger-" (:container-id opts) "-" (:db/id block) "-" (:db/id property))
  821. show! (fn [e]
  822. (let [target (.-target e)]
  823. (when-not (or config/publishing?
  824. (util/shift-key? e)
  825. (util/meta-key? e)
  826. (util/link? target)
  827. (when-let [node (.closest target "a")]
  828. (not (or (d/has-class? node "page-ref")
  829. (d/has-class? node "tag")))))
  830. (shui/popup-show! target popup-content
  831. {:align "start"
  832. :as-dropdown? true
  833. :auto-focus? true
  834. :trigger-id trigger-id}))))]
  835. (shui/trigger-as
  836. (if (:other-position? opts) :div :div.jtrigger.flex.flex-1.w-full)
  837. {:ref *el
  838. :id trigger-id
  839. :tabIndex 0
  840. :on-click show!}
  841. (if (string/blank? value)
  842. (property-empty-text-value)
  843. (value-f))))))
  844. (defn- property-value-inner
  845. [block property value {:keys [inline-text page-cp
  846. dom-id row?]}]
  847. (let [schema (:block/schema property)
  848. multiple-values? (db-property/many? property)
  849. class (str (when-not row? "flex flex-1 ")
  850. (when multiple-values? "property-value-content"))
  851. type (:type schema)]
  852. [:div.cursor-text
  853. {:id (or dom-id (random-uuid))
  854. :tabIndex 0
  855. :class (str class " " (when-not (= type :default) "jtrigger"))
  856. :style {:min-height 24}
  857. :on-click (fn []
  858. (when (and (= type :default) (nil? value))
  859. (<create-new-block! block property "")))}
  860. (cond
  861. (and (= type :default) (nil? (:block/title value)))
  862. [:div.jtrigger (property-empty-btn-value property)]
  863. (= type :default)
  864. (property-block-value value block property page-cp)
  865. :else
  866. (inline-text {} :markdown (macro-util/expand-value-if-macro (str value) (state/get-macros))))]))
  867. (rum/defcs property-scalar-value < rum/reactive db-mixins/query rum/static
  868. [state block property value* {:keys [container-id editing? on-chosen]
  869. :as opts}]
  870. (let [property (model/sub-block (:db/id property))
  871. block (db/sub-block (:db/id block))
  872. schema (:block/schema property)
  873. type (get schema :type :default)
  874. editing? (or editing?
  875. (and (state/sub-editing? [container-id (:block/uuid block)])
  876. (= (:db/id property) (:db/id (:property (state/get-editor-action-data))))))
  877. select-type?' (select-type? property type)
  878. closed-values? (seq (:property/closed-values property))
  879. select-opts {:on-chosen on-chosen}
  880. value (if (and (de/entity? value*) (= (:db/ident value*) :logseq.property/empty-placeholder))
  881. nil
  882. value*)]
  883. (if (= :logseq.property/icon (:db/ident property))
  884. (icon-row block editing?)
  885. (if (and select-type?'
  886. (not (and (not closed-values?) (= type :date))))
  887. (let [value (if (and (nil? value) (= :logseq.property.view/type (:db/ident property)))
  888. ;; TODO: remove this hack once default value is supported
  889. (db/entity :logseq.property.view/type.table)
  890. value)]
  891. (single-value-select block property value
  892. (fn [] (select-item property type value opts))
  893. select-opts
  894. (assoc opts :editing? editing?)))
  895. (case type
  896. (:date :datetime)
  897. (property-value-date-picker block property value (merge opts {:editing? editing?}))
  898. :checkbox
  899. (let [add-property! (fn [] (<add-property! block (:db/ident property) (boolean (not value))))]
  900. [:label.flex.w-full.as-scalar-value-wrap.cursor-pointer
  901. (shui/checkbox {:class "jtrigger flex flex-row items-center"
  902. :disabled config/publishing?
  903. :auto-focus editing?
  904. :checked value
  905. :on-checked-change add-property!
  906. :on-key-down (fn [e]
  907. (when (= (util/ekey e) "Enter")
  908. (add-property!)))})])
  909. ;; :others
  910. [:div.flex.flex-1
  911. (property-value-inner block property value opts)])))))
  912. (rum/defc multiple-values-inner
  913. [block property v {:keys [on-chosen editing?] :as opts} schema]
  914. (let [type (get schema :type :default)
  915. date? (= type :date)
  916. *el (rum/use-ref nil)
  917. items (if (de/entity? v) #{v} v)]
  918. (rum/use-effect!
  919. (fn []
  920. (when editing?
  921. (.click (rum/deref *el))))
  922. [editing?])
  923. (let [select-cp (fn [select-opts]
  924. (let [select-opts (merge {:multiple-choices? true
  925. :on-chosen (fn []
  926. (when on-chosen (on-chosen)))}
  927. select-opts
  928. {:dropdown? false})]
  929. [:div.property-select
  930. (if (contains? #{:node :page :class :property} type)
  931. (property-value-select-node block property
  932. select-opts
  933. opts)
  934. (select block property select-opts opts))]))]
  935. (let [toggle-fn shui/popup-hide!
  936. content-fn (fn [{:keys [_id content-props]}]
  937. (select-cp {:content-props content-props}))]
  938. [:div.multi-values.jtrigger
  939. {:tab-index "0"
  940. :ref *el
  941. :on-click (fn [^js e]
  942. (let [target (.-target e)]
  943. (when-not (or (util/link? target) (.closest target "a") config/publishing?)
  944. (shui/popup-show! (rum/deref *el) content-fn
  945. {:as-dropdown? true :as-content? false
  946. :align "start" :auto-focus? true}))))
  947. :on-key-down (fn [^js e]
  948. (case (.-key e)
  949. (" " "Enter")
  950. (do (some-> (rum/deref *el) (.click))
  951. (util/stop e))
  952. :dune))
  953. :class "flex flex-1 flex-row items-center flex-wrap gap-x-2 gap-y-2 pr-4"}
  954. (let [not-empty-value? (not= (map :db/ident items) [:logseq.property/empty-placeholder])]
  955. (if (and (seq items) not-empty-value?)
  956. (concat
  957. (->> (for [item items]
  958. (rum/with-key (select-item property type item opts) (or (:block/uuid item) (str item))))
  959. (interpose [:span.opacity-50.-ml-2 ","]))
  960. (when date?
  961. [(property-value-date-picker block property nil {:toggle-fn toggle-fn})]))
  962. (if date?
  963. (property-value-date-picker block property nil {:toggle-fn toggle-fn})
  964. (property-empty-text-value))))]))))
  965. (rum/defc multiple-values < rum/reactive db-mixins/query
  966. [block property opts schema]
  967. (let [block (db/sub-block (:db/id block))
  968. value (get block (:db/ident property))
  969. value' (if (coll? value) value
  970. (when (some? value) #{value}))]
  971. (multiple-values-inner block property value' opts schema)))
  972. (rum/defcs property-value < rum/reactive
  973. [state block property v {:keys [show-tooltip?]
  974. :as opts}]
  975. (ui/catch-error
  976. (ui/block-error "Something wrong" {})
  977. (let [block-cp (state/get-component :block/blocks-container)
  978. properties-cp (state/get-component :block/properties-cp)
  979. opts (merge opts
  980. {:page-cp (state/get-component :block/page-cp)
  981. :inline-text (state/get-component :block/inline-text)
  982. :editor-box (state/get-component :editor/box)
  983. :block-cp block-cp
  984. :properties-cp :properties-cp})
  985. dom-id (str "ls-property-" (:db/id block) "-" (:db/id property))
  986. editor-id (str dom-id "-editor")
  987. schema (:block/schema property)
  988. type (some-> schema (get :type :default))
  989. multiple-values? (db-property/many? property)
  990. v (cond
  991. (and multiple-values? (or (set? v) (and (coll? v) (empty? v)) (nil? v)))
  992. v
  993. multiple-values?
  994. #{v}
  995. (set? v)
  996. (first v)
  997. :else
  998. v)
  999. empty-value? (when (coll? v) (= :logseq.property/empty-placeholder (:db/ident (first v))))
  1000. closed-values? (seq (:property/closed-values property))
  1001. value-cp [:div.property-value-inner
  1002. {:data-type type
  1003. :class (str (when empty-value? "empty-value")
  1004. (when-not (:other-position? opts) " w-full"))}
  1005. (cond
  1006. (= (:db/ident property) :logseq.property.class/properties)
  1007. (properties-cp {} block {:selected? false
  1008. :class-schema? true})
  1009. (and multiple-values? (= type :default) (not closed-values?))
  1010. (property-normal-block-value block property v)
  1011. multiple-values?
  1012. (multiple-values block property opts schema)
  1013. :else
  1014. (let [parent? (= (:db/ident property) :logseq.property/parent)
  1015. value-cp (property-scalar-value block property v
  1016. (merge
  1017. opts
  1018. {:editor-id editor-id
  1019. :dom-id dom-id}))
  1020. page-ancestors (when parent?
  1021. (let [ancestor-pages (loop [parents [block]]
  1022. (if-let [parent (:logseq.property/parent (last parents))]
  1023. (when-not (contains? (set parents) parent)
  1024. (recur (conj parents parent)))
  1025. parents))]
  1026. (->> (reverse ancestor-pages)
  1027. (remove (fn [e] (= (:db/id block) (:db/id e))))
  1028. butlast)))]
  1029. (if (seq page-ancestors)
  1030. [:div.flex.flex-1.items-center.gap-1
  1031. (interpose [:span.opacity-50.text-sm " > "]
  1032. (concat
  1033. (map (fn [{title :block/title :as ancestor}]
  1034. [:a {:on-click #(route-handler/redirect-to-page! (:block/uuid ancestor))} title])
  1035. page-ancestors)
  1036. [value-cp]))]
  1037. value-cp)))]]
  1038. (if show-tooltip?
  1039. (shui/tooltip-provider
  1040. (shui/tooltip
  1041. {:delayDuration 1200}
  1042. (shui/tooltip-trigger
  1043. {:onFocusCapture #(util/stop-propagation %)} value-cp)
  1044. (shui/tooltip-content
  1045. (str "Change " (:block/title property)))))
  1046. value-cp))))