property.cljs 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. (ns frontend.components.property
  2. "Block properties management."
  3. (:require [clojure.set :as set]
  4. [clojure.string :as string]
  5. [frontend.components.dnd :as dnd]
  6. [frontend.components.icon :as icon-component]
  7. [frontend.components.property.config :as property-config]
  8. [frontend.components.property.value :as pv]
  9. [frontend.components.select :as select]
  10. [frontend.components.svg :as svg]
  11. [frontend.config :as config]
  12. [frontend.db :as db]
  13. [frontend.db-mixins :as db-mixins]
  14. [frontend.db.async :as db-async]
  15. [frontend.db.model :as db-model]
  16. [frontend.handler.db-based.property :as db-property-handler]
  17. [frontend.handler.notification :as notification]
  18. [frontend.handler.property :as property-handler]
  19. [frontend.handler.property.util :as pu]
  20. [frontend.handler.route :as route-handler]
  21. [frontend.mixins :as mixins]
  22. [frontend.modules.shortcut.core :as shortcut]
  23. [frontend.state :as state]
  24. [frontend.ui :as ui]
  25. [frontend.util :as util]
  26. [logseq.db :as ldb]
  27. [logseq.db.common.order :as db-order]
  28. [logseq.db.frontend.entity-util :as entity-util]
  29. [logseq.db.frontend.property :as db-property]
  30. [logseq.db.frontend.property.type :as db-property-type]
  31. [logseq.outliner.core :as outliner-core]
  32. [logseq.outliner.property :as outliner-property]
  33. [logseq.shui.hooks :as hooks]
  34. [logseq.shui.ui :as shui]
  35. [promesa.core :as p]
  36. [rum.core :as rum]))
  37. (defn- <add-property-from-dropdown
  38. "Adds an existing or new property from dropdown. Used from a block or page context."
  39. [entity property-uuid-or-name schema {:keys [class-schema?]}]
  40. (p/let [repo (state/get-current-repo)
  41. ;; Both conditions necessary so that a class can add its own page properties
  42. add-class-property? (and (ldb/class? entity) class-schema?)
  43. result (when (uuid? property-uuid-or-name)
  44. (db-async/<get-block repo property-uuid-or-name {:children? false}))
  45. ;; In block context result is in :block
  46. property (some-> (if (:block result) (:db/id (:block result)) (:db/id result))
  47. db/entity)]
  48. ;; existing property selected or entered
  49. (if property
  50. (do
  51. (when (and (not (ldb/public-built-in-property? property))
  52. (ldb/built-in? property))
  53. (notification/show! "This is a private built-in property that can't be used." :error))
  54. property)
  55. ;; new property entered
  56. (if (db-property/valid-property-name? property-uuid-or-name)
  57. (if add-class-property?
  58. (p/let [result (db-property-handler/upsert-property! nil schema {:property-name property-uuid-or-name})
  59. property (db/entity (:db/id result))
  60. _ (pv/<add-property! entity (:db/ident property) "" {:class-schema? class-schema? :exit-edit? false})]
  61. property)
  62. (p/let [result (db-property-handler/upsert-property! nil schema {:property-name property-uuid-or-name})]
  63. (db/entity (:db/id result))))
  64. (notification/show! "This is an invalid property name. A property name cannot start with page reference characters '#' or '[['." :error)))))
  65. ;; TODO: This component should be cleaned up as it's only used for new properties and used to be used for existing properties
  66. (rum/defcs property-type-select <
  67. shortcut/disable-all-shortcuts
  68. [state property {:keys [*property *property-name *property-schema built-in? disabled?
  69. show-type-change-hints? block *show-new-property-config?
  70. *show-class-select?
  71. default-open? class-schema?]
  72. :as opts}]
  73. (let [property-name (or (and *property-name @*property-name) (:block/title property))
  74. property-schema (or (and *property-schema @*property-schema)
  75. (select-keys property [:logseq.property/type]))
  76. schema-types (->> (concat db-property-type/user-built-in-property-types
  77. (when built-in?
  78. db-property-type/internal-built-in-property-types))
  79. (map (fn [type]
  80. {:label (property-config/property-type-label type)
  81. :value type})))]
  82. [:div {:class "flex items-center"}
  83. (shui/select
  84. (cond->
  85. {:default-open (boolean default-open?)
  86. :disabled disabled?
  87. :on-value-change
  88. (fn [v]
  89. (let [type (keyword (string/lower-case v))
  90. update-schema-fn #(assoc % :logseq.property/type type)]
  91. (when *property-schema
  92. (swap! *property-schema update-schema-fn))
  93. (let [schema (or (and *property-schema @*property-schema)
  94. (update-schema-fn property-schema))]
  95. (when *show-new-property-config?
  96. (reset! *show-new-property-config? :adding-property))
  97. (p/let [property' (when block (<add-property-from-dropdown block property-name schema opts))
  98. property (or property' property)
  99. add-class-property? (and (ldb/class? block) class-schema?)]
  100. (when *property (reset! *property property))
  101. (p/do!
  102. (when *show-new-property-config?
  103. (reset! *show-new-property-config? false))
  104. (when (= (:logseq.property/type schema) :node) (reset! *show-class-select? true))
  105. (db-property-handler/upsert-property!
  106. (:db/ident property)
  107. schema
  108. {})
  109. (cond
  110. (and *show-class-select? @*show-class-select?)
  111. nil
  112. add-class-property?
  113. (do
  114. (shui/popup-hide!)
  115. (shui/dialog-close!))
  116. (pv/batch-operation?)
  117. nil
  118. (and block (= type :checkbox))
  119. (p/do!
  120. (ui/hide-popups-until-preview-popup!)
  121. (let [value (if-some [value (:logseq.property/scalar-default-value property)]
  122. value
  123. false)]
  124. (pv/<add-property! block (:db/ident property) value {:exit-edit? true})))
  125. (and block
  126. (contains? #{:default :url} type)
  127. (not (seq (:property/closed-values property))))
  128. (pv/<create-new-block! block property "" {:batch-op? true})))))))}
  129. ;; only set when in property configure modal
  130. (and *property-name (:logseq.property/type property-schema))
  131. (assoc :default-value (name (:logseq.property/type property-schema))))
  132. (shui/select-trigger
  133. {:class "!px-2 !py-0 !h-8"}
  134. (shui/select-value
  135. {:placeholder "Select a property type"}))
  136. (shui/select-content
  137. (shui/select-group
  138. (for [{:keys [label value disabled]} schema-types]
  139. (shui/select-item {:key label :value value :disabled disabled
  140. :on-key-down (fn [e]
  141. (when (= "Enter" (.-key e))
  142. (util/stop-propagation e)))} label)))))
  143. (when show-type-change-hints?
  144. (ui/tippy {:html "Changing the property type clears some property configurations."
  145. :class "tippy-hover ml-2"
  146. :interactive true
  147. :disabled false}
  148. (svg/info)))]))
  149. (rum/defc property-select
  150. [exclude-properties select-opts]
  151. (let [[properties set-properties!] (rum/use-state nil)
  152. [classes set-classes!] (rum/use-state nil)
  153. [excluded-properties set-excluded-properties!] (rum/use-state nil)]
  154. (hooks/use-effect!
  155. (fn []
  156. (p/let [repo (state/get-current-repo)
  157. properties (db-async/<db-based-get-all-properties repo)
  158. classes (->> (db-model/get-all-classes repo)
  159. (remove ldb/built-in?))]
  160. (set-classes! classes)
  161. (set-properties! (remove exclude-properties properties))
  162. (set-excluded-properties! (->> properties
  163. (filter exclude-properties)
  164. (map :block/title)
  165. set))))
  166. [])
  167. (let [items (->>
  168. (concat
  169. (map (fn [x]
  170. {:label (:block/title x)
  171. :value (:block/uuid x)}) properties)
  172. (map (fn [x]
  173. {:label (:block/title x)
  174. :value (:block/uuid x)
  175. :group "Tags"}) classes))
  176. (util/distinct-by-last-wins :value))]
  177. [:div.ls-property-add.flex.flex-row.items-center.property-key
  178. {:data-keep-selection true}
  179. [:div.ls-property-key
  180. (select/select (merge
  181. {:items items
  182. :grouped? true
  183. :extract-fn :label
  184. :dropdown? false
  185. :close-modal? false
  186. :new-case-sensitive? true
  187. :show-new-when-not-exact-match? true
  188. :exact-match-exclude-items (fn [s] (contains? excluded-properties s))
  189. :input-default-placeholder "Add or change property"}
  190. select-opts))]])))
  191. (rum/defc property-icon
  192. [property property-type]
  193. (let [type (or (:logseq.property/type property) property-type :default)
  194. ident (:db/ident property)
  195. icon (cond
  196. (= ident :block/tags)
  197. "hash"
  198. (string/starts-with? (str ident) ":plugin.")
  199. "puzzle"
  200. :else
  201. (case type
  202. :number "number"
  203. :date "calendar"
  204. :datetime "calendar"
  205. :checkbox "checkbox"
  206. :url "link"
  207. :page "page"
  208. :node "letter-n"
  209. "letter-t"))]
  210. (ui/icon icon {:class "opacity-50"
  211. :size 15})))
  212. (defn- property-input-on-chosen
  213. [block *property *property-key *show-new-property-config? {:keys [class-schema? remove-property?]}]
  214. (fn [{:keys [value label]}]
  215. (reset! *property-key (if (uuid? value) label value))
  216. (let [property (when (uuid? value) (db/entity [:block/uuid value]))
  217. batch? (pv/batch-operation?)
  218. repo (state/get-current-repo)]
  219. (if (and property remove-property?)
  220. (let [block-ids (map :block/uuid (pv/get-operating-blocks block))]
  221. (property-handler/batch-remove-block-property! repo block-ids (:db/ident property))
  222. (shui/popup-hide!))
  223. (do
  224. (when (and *show-new-property-config? (not (ldb/property? property)))
  225. (reset! *show-new-property-config? true))
  226. (reset! *property property)
  227. (when property
  228. (let [add-class-property? (and (ldb/class? block) class-schema?)
  229. type (:logseq.property/type property)
  230. default-or-url? (and (contains? #{:default :url} type)
  231. (not (seq (:property/closed-values property))))]
  232. (cond
  233. add-class-property?
  234. (p/do!
  235. (pv/<add-property! block (:db/ident property) "" {:class-schema? class-schema?})
  236. (shui/popup-hide!)
  237. (shui/dialog-close!))
  238. ;; using class as property
  239. (and property (ldb/class? property))
  240. (p/do!
  241. (pv/<set-class-as-property! (state/get-current-repo) property)
  242. (reset! *show-new-property-config? false))
  243. (and batch? (or (= :checkbox type) (and batch? default-or-url?)))
  244. nil
  245. (= :checkbox type)
  246. (p/do!
  247. (ui/hide-popups-until-preview-popup!)
  248. (shui/popup-hide!)
  249. (shui/dialog-close!)
  250. (let [value (if-some [value (:logseq.property/scalar-default-value property)]
  251. value
  252. false)]
  253. (pv/<add-property! block (:db/ident property) value {:exit-edit? true})))
  254. default-or-url?
  255. (pv/<create-new-block! block property "" {:batch-op? true})
  256. (or (not= :default type)
  257. (and (= :default type) (seq (:property/closed-values property))))
  258. (reset! *show-new-property-config? false)))))))))
  259. (rum/defc property-key-title
  260. [block property class-schema?]
  261. (shui/trigger-as
  262. :a
  263. {:tabIndex 0
  264. :title (:block/title property)
  265. :class "property-k flex select-none jtrigger w-full"
  266. :on-pointer-down (fn [^js e]
  267. (when (util/meta-key? e)
  268. (route-handler/redirect-to-page! (:block/uuid property))
  269. (.preventDefault e)))
  270. :on-click (fn [^js/MouseEvent e]
  271. (shui/popup-show! (.-target e)
  272. (fn []
  273. (property-config/dropdown-editor property block {:debug? (.-altKey e)
  274. :class-schema? class-schema?}))
  275. {:content-props
  276. {:class "ls-property-dropdown-editor as-root"
  277. :onEscapeKeyDown (fn [e]
  278. (util/stop e)
  279. (shui/popup-hide!)
  280. (when-let [input (state/get-input)]
  281. (.focus input)))}
  282. :align "start"
  283. :as-dropdown? true}))}
  284. (:block/title property)))
  285. (rum/defc property-key-cp < rum/static
  286. [block property {:keys [other-position? class-schema?]}]
  287. (let [icon (:logseq.property/icon property)]
  288. [:div.property-key-inner.jtrigger-view
  289. ;; icon picker
  290. (when-not other-position?
  291. (let [content-fn (fn [{:keys [id]}]
  292. (icon-component/icon-search
  293. {:on-chosen
  294. (fn [_e icon]
  295. (if icon
  296. (db-property-handler/set-block-property! (:db/id property)
  297. :logseq.property/icon icon)
  298. (db-property-handler/remove-block-property! (:db/id property)
  299. (pu/get-pid :logseq.property/icon)))
  300. (shui/popup-hide! id))
  301. :icon-value icon
  302. :del-btn? (boolean icon)}))]
  303. [:div.property-icon
  304. (shui/trigger-as
  305. :button.property-m
  306. (-> (when-not config/publishing?
  307. {:on-click (fn [^js e]
  308. (shui/popup-show! (.-target e) content-fn
  309. {:as-dropdown? true :auto-focus? true
  310. :content-props {:onEscapeKeyDown #(.preventDefault %)}}))})
  311. (assoc :class "flex items-center"))
  312. (if icon
  313. (icon-component/icon icon {:size 15 :color? true})
  314. (property-icon property nil)))]))
  315. (if config/publishing?
  316. [:a.property-k.flex.select-none.jtrigger
  317. {:on-click #(route-handler/redirect-to-page! (:block/uuid property))}
  318. (:block/title property)]
  319. (property-key-title block property class-schema?))]))
  320. (rum/defcs ^:large-vars/cleanup-todo property-input < rum/reactive
  321. (rum/local false ::show-new-property-config?)
  322. (rum/local false ::show-class-select?)
  323. (rum/local {} ::property-schema)
  324. (mixins/event-mixin
  325. (fn [state]
  326. (mixins/hide-when-esc-or-outside
  327. state
  328. :on-hide (fn [_state _e type]
  329. (when (contains? #{:esc} type)
  330. (shui/popup-hide!)
  331. (shui/popup-hide!)
  332. (shui/dialog-close!)
  333. (when-let [^js input (state/get-input)]
  334. (.focus input)))))))
  335. {:init (fn [state]
  336. (state/set-editor-action! :property-input)
  337. (assoc state ::property (or (:*property (last (:rum/args state)))
  338. (atom nil))))
  339. :will-unmount (fn [state]
  340. (let [args (:rum/args state)
  341. *property-key (second args)
  342. {:keys [original-block edit-original-block]} (last args)
  343. editing-default-property? (and original-block (state/get-edit-block)
  344. (not= (:db/id original-block) (:db/id (state/get-edit-block))))]
  345. (when *property-key (reset! *property-key nil))
  346. (when (and original-block edit-original-block)
  347. (edit-original-block {:editing-default-property? editing-default-property?})))
  348. (state/set-editor-action! nil)
  349. state)}
  350. [state block *property-key {:keys [class-schema?]
  351. :as opts}]
  352. (let [*property (::property state)
  353. *show-new-property-config? (::show-new-property-config? state)
  354. *show-class-select? (::show-class-select? state)
  355. *property-schema (::property-schema state)
  356. page? (entity-util/page? block)
  357. block-types (let [types (ldb/get-entity-types block)]
  358. (cond-> types
  359. (and page? (not (contains? types :page)))
  360. (conj :page)
  361. (empty? types)
  362. #{:block}))
  363. exclude-properties (fn [m]
  364. (let [view-context (get m :logseq.property/view-context :all)]
  365. (or (contains? #{:logseq.property/query} (:db/ident m))
  366. (and (not page?) (contains? #{:block/alias} (:db/ident m)))
  367. ;; Filters out properties from being in wrong :view-context and :never view-contexts
  368. (and (not= view-context :all) (not (contains? block-types view-context)))
  369. (and (ldb/built-in? block) (contains? #{:logseq.property/parent} (:db/ident m)))
  370. ;; Filters out adding buggy class properties e.g. Alias and Parent
  371. (and class-schema? (ldb/public-built-in-property? m) (:logseq.property/view-context m)))))
  372. property (rum/react *property)
  373. property-key (rum/react *property-key)
  374. batch? (pv/batch-operation?)
  375. hide-property-key? (or (contains? #{:date :datetime} (:logseq.property/type property))
  376. (pv/select-type? block property)
  377. (and
  378. batch?
  379. (contains? #{:default :url} (:logseq.property/type property))
  380. (not (seq (:property/closed-values property))))
  381. (and property (ldb/class? property)))]
  382. [:div.ls-property-input.flex.flex-1.flex-row.items-center.flex-wrap.gap-1
  383. (if property-key
  384. [:div.ls-property-add.gap-1.flex.flex-1.flex-row.items-center
  385. (when-not hide-property-key?
  386. [:div.flex.flex-row.items-center.property-key.gap-1
  387. (when-not (:db/id property) (property-icon property (:logseq.property/type @*property-schema)))
  388. (if (:db/id property) ; property exists already
  389. (property-key-cp block property opts)
  390. [:div property-key])])
  391. [:div.flex.flex-row {:on-pointer-down (fn [e] (util/stop-propagation e))}
  392. (when (not= @*show-new-property-config? :adding-property)
  393. (cond
  394. @*show-new-property-config?
  395. (property-type-select property (merge opts
  396. {:*property *property
  397. :*property-name *property-key
  398. :*property-schema *property-schema
  399. :default-open? true
  400. :block block
  401. :*show-new-property-config? *show-new-property-config?
  402. :*show-class-select? *show-class-select?}))
  403. (and property @*show-class-select?)
  404. (property-config/class-select property (assoc opts
  405. :on-hide #(reset! *show-class-select? false)
  406. :multiple-choices? false
  407. :default-open? true
  408. :no-class? true))
  409. :else
  410. (when (and property (not class-schema?))
  411. (pv/property-value block property (assoc opts :editing? true)))))]]
  412. (let [on-chosen (property-input-on-chosen block *property *property-key *show-new-property-config? opts)
  413. input-opts {:on-key-down
  414. (fn [e]
  415. ;; `Backspace` to close property popup and back to editing the current block
  416. (when (and (= (util/ekey e) "Backspace")
  417. (= "" (.-value (.-target e))))
  418. (util/stop e)
  419. (shui/popup-hide!)))}]
  420. (property-select exclude-properties
  421. (merge (:select-opts opts) {:on-chosen on-chosen
  422. :input-opts input-opts}))))]))
  423. (rum/defcs new-property < rum/reactive
  424. [state block opts]
  425. (when-not config/publishing?
  426. [:div.ls-new-property {:style {:margin-left 6 :margin-top 1}}
  427. [:a.fade-link.flex.jtrigger
  428. {:tab-index 0
  429. :on-click (fn [e]
  430. (state/pub-event! [:editor/new-property (merge opts {:block block
  431. :target (.-target e)})]))}
  432. [:div.flex.flex-row.items-center.shrink-0
  433. (ui/icon "plus" {:size 16})
  434. [:div.ml-1
  435. "Add property"]]]]))
  436. (defn- resolve-linked-block-if-exists
  437. "Properties will be updated for the linked page instead of the refed block.
  438. For example, the block below has a reference to the page \"How to solve it\",
  439. we'd like the properties of the class \"book\" (e.g. Authors, Published year)
  440. to be assigned for the page `How to solve it` instead of the referenced block.
  441. Block:
  442. - [[How to solve it]] #book
  443. "
  444. [block]
  445. (if-let [linked-block (:block/link block)]
  446. (db/sub-block (:db/id linked-block))
  447. (db/sub-block (:db/id block))))
  448. (rum/defc property-cp <
  449. rum/reactive
  450. db-mixins/query
  451. [block k v {:keys [inline-text page-cp sortable-opts] :as opts}]
  452. (when (keyword? k)
  453. (when-let [property (db/sub-block (:db/id (db/entity k)))]
  454. (let [type (get property :logseq.property/type :default)
  455. closed-values? (seq (:property/closed-values property))
  456. block? (and v
  457. (not closed-values?)
  458. (or (and (map? v) (:block/page v))
  459. (and (coll? v)
  460. (map? (first v))
  461. (or (:block/page (first v))
  462. (= :logseq.property/empty-placeholder (:db/ident (first v))))))
  463. (contains? #{:default :url} type))
  464. date? (= type :date)
  465. datetime? (= type :datetime)
  466. checkbox? (= type :checkbox)
  467. property-key-cp' (property-key-cp block property (assoc (select-keys opts [:class-schema?])
  468. :block? block?
  469. :inline-text inline-text
  470. :page-cp page-cp))]
  471. [:div {:key (str "property-pair-" (:db/id block) "-" (:db/id property))
  472. :class (cond
  473. (= (:db/ident property) :logseq.property.class/properties)
  474. "property-pair !flex !flex-col"
  475. (or date? datetime? checkbox?)
  476. "property-pair items-center"
  477. :else
  478. "property-pair items-start")}
  479. (if (seq sortable-opts)
  480. (dnd/sortable-item (assoc sortable-opts :class "property-key") property-key-cp')
  481. [:div.property-key property-key-cp'])
  482. (let [class-properties? (= (:db/ident property) :logseq.property.class/properties)
  483. property-desc (when-not (= (:db/ident property) :logseq.property/description)
  484. (:logseq.property/description property))]
  485. [:div.property-value-container.flex.flex-row.gap-1.items-center
  486. (cond-> {}
  487. class-properties? (assoc :class (if (:logseq.property.class/properties block)
  488. "ml-2 -mt-1"
  489. "-ml-1")))
  490. (when-not (or block? class-properties? (and property-desc (:class-schema? opts)))
  491. [:div {:class "pl-1.5 -mr-[3px] opacity-60"}
  492. [:span.bullet-container [:span.bullet]]])
  493. [:div.flex.flex-1
  494. [:div.property-value.flex.flex-1
  495. (cond-> {}
  496. class-properties? (assoc :class :opacity-90))
  497. (if (:class-schema? opts)
  498. (pv/property-value property (db/entity :logseq.property/description) opts)
  499. (pv/property-value block property opts))]]])]))))
  500. (rum/defcs ordered-properties < rum/reactive
  501. {:init (fn [state]
  502. (assoc state ::properties-order (atom (mapv first (second (:rum/args state))))))
  503. :should-update (fn [old-state new-state]
  504. (let [[_ p1 opts1] (:rum/args old-state)
  505. [_ p2 opts2] (:rum/args new-state)
  506. p1-keys (map first p1)
  507. p1-set (set p1-keys)
  508. p1-m (zipmap (map first p1) (map second p1))
  509. p2-m (zipmap (map first p2) (map second p2))
  510. p2-set (set (map first p2))]
  511. (when-not (= p1-set p2-set)
  512. (reset! (::properties-order new-state) (mapv first p2)))
  513. (not= [p1-set (map p1-m p1-keys) opts1] [p2-set (map p2-m p1-keys) opts2])))}
  514. [state block properties opts]
  515. (let [*properties-order (::properties-order state)
  516. properties-order (rum/react *properties-order)
  517. m (zipmap (map first properties) (map second properties))
  518. properties (mapv (fn [k] [k (get m k)]) properties-order)
  519. choices (map (fn [[k v]]
  520. (let [id (subs (str k) 1)
  521. opts (assoc opts :sortable-opts {:id id})]
  522. {:id id
  523. :value k
  524. :content (property-cp block k v opts)})) properties)]
  525. (dnd/items choices
  526. {:sort-by-inner-element? true
  527. :on-drag-end (fn [properties-order {:keys [active-id over-id direction]}]
  528. (let [move-down? (= direction :down)
  529. over (db/entity (keyword over-id))
  530. active (db/entity (keyword active-id))
  531. over-order (:block/order over)
  532. new-order (if move-down?
  533. (let [next-order (db-order/get-next-order (db/get-db) nil (:db/id over))]
  534. (db-order/gen-key over-order next-order))
  535. (let [prev-order (db-order/get-prev-order (db/get-db) nil (:db/id over))]
  536. (db-order/gen-key prev-order over-order)))]
  537. ;; Reset *properties-order without waiting for `db/transact!` so that the UI will not be
  538. ;; converted back to the old order and then the new order.
  539. (reset! *properties-order properties-order)
  540. (db/transact! (state/get-current-repo)
  541. [{:db/id (:db/id active)
  542. :block/order new-order}
  543. (outliner-core/block-with-updated-at
  544. {:db/id (:db/id block)})]
  545. {:outliner-op :save-block})))})))
  546. (rum/defc properties-section < rum/reactive db-mixins/query
  547. [block properties opts]
  548. (when (seq properties)
  549. ;; Sort properties by :block/order
  550. (let [properties' (sort-by (fn [[k _v]]
  551. (if (= k :logseq.property.class/properties)
  552. "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
  553. (:block/order (db/entity k)))) properties)]
  554. (ordered-properties block properties' opts))))
  555. (defn- async-load-classes!
  556. [block]
  557. (let [repo (state/get-current-repo)
  558. classes (concat (:block/tags block) (outliner-property/get-classes-parents (:block/tags block)))]
  559. (doseq [class classes]
  560. (db-async/<get-block repo (:db/id class) :children? false))
  561. (when (ldb/class? block)
  562. (doseq [property (:logseq.property.class/properties block)]
  563. (db-async/<get-block repo (:db/id property) :children? false)))
  564. classes))
  565. (rum/defcs ^:large-vars/cleanup-todo properties-area < rum/reactive db-mixins/query
  566. {:init (fn [state]
  567. (let [target-block (first (:rum/args state))
  568. block (resolve-linked-block-if-exists target-block)]
  569. (assoc state
  570. ::id (str (random-uuid))
  571. ::block block
  572. ::classes (async-load-classes! block))))
  573. :will-remount (fn [state]
  574. (let [block (db/entity (:db/id (::block state)))]
  575. (assoc state ::classes (async-load-classes! block))))}
  576. [state _target-block {:keys [sidebar-properties?] :as opts}]
  577. (let [id (::id state)
  578. db-id (:db/id (::block state))
  579. block (db/sub-block db-id)
  580. show-empty-and-hidden-properties? (let [{:keys [mode show? ids]} (state/sub :ui/show-empty-and-hidden-properties?)]
  581. (and show?
  582. (or (= mode :global)
  583. (and (set? ids) (contains? ids (:block/uuid block))))))
  584. _ (doseq [class (::classes state)]
  585. (db/sub-block (:db/id class)))
  586. class? (ldb/class? block)
  587. properties (:block/properties block)
  588. remove-built-in-or-other-position-properties
  589. (fn [properties]
  590. (remove (fn [property]
  591. (let [id (if (vector? property) (first property) property)]
  592. (or
  593. (= id :block/tags)
  594. (when-let [ent (db/entity id)]
  595. (or
  596. ;; built-in
  597. (and (not (ldb/public-built-in-property? ent))
  598. ;; TODO: Use ldb/built-in? when intermittent lazy loading issue fixed
  599. (get db-property/built-in-properties (:db/ident ent)))
  600. ;; other position
  601. (when-not (or (and (:sidebar? opts) (= (:id opts) (str (:block/uuid block))))
  602. show-empty-and-hidden-properties?)
  603. (outliner-property/property-with-other-position? ent))
  604. (and (:gallery-view? opts)
  605. (contains? #{:logseq.property.class/properties} (:db/ident ent))))))))
  606. properties))
  607. {:keys [all-classes classes-properties]} (outliner-property/get-block-classes-properties (db/get-db) (:db/id block))
  608. classes-properties-set (set (map :db/ident classes-properties))
  609. block-own-properties (->> properties
  610. (remove (fn [[id _]] (classes-properties-set id))))
  611. root-block? (= (:id opts) (str (:block/uuid block)))
  612. state-hide-empty-properties? (:ui/hide-empty-properties? (state/get-config))
  613. ;; This section produces own-properties and full-hidden-properties
  614. hide-with-property-id (fn [property-id]
  615. (let [property (db/entity property-id)]
  616. (cond
  617. show-empty-and-hidden-properties?
  618. false
  619. root-block?
  620. false
  621. (and (:logseq.property/hide-empty-value property)
  622. (nil? (get properties property-id)))
  623. true
  624. state-hide-empty-properties?
  625. (nil? (get block property-id))
  626. :else
  627. (boolean (:logseq.property/hide? property)))))
  628. property-hide-f (cond
  629. config/publishing?
  630. ;; Publishing is read only so hide all blank properties as they
  631. ;; won't be edited and distract from properties that have values
  632. (fn [[property-id property-value]]
  633. (or (nil? property-value)
  634. (hide-with-property-id property-id)))
  635. state-hide-empty-properties?
  636. (fn [[property-id property-value]]
  637. ;; User's selection takes precedence over config
  638. (if (:logseq.property/hide? (db/entity property-id))
  639. (hide-with-property-id property-id)
  640. (nil? property-value)))
  641. :else
  642. (comp hide-with-property-id first))
  643. {_block-hidden-properties true
  644. block-own-properties' false} (group-by property-hide-f block-own-properties)
  645. class-properties (loop [classes all-classes
  646. properties (set (map first block-own-properties'))
  647. result []]
  648. (if-let [class (first classes)]
  649. (let [cur-properties (->> (db-property/get-class-ordered-properties class)
  650. (map :db/ident)
  651. (remove properties)
  652. (remove hide-with-property-id))]
  653. (recur (rest classes)
  654. (set/union properties (set cur-properties))
  655. (if (seq cur-properties)
  656. (into result cur-properties)
  657. result)))
  658. result))
  659. full-properties (->> (concat block-own-properties'
  660. (map (fn [p] [p (get block p)]) class-properties)
  661. (when (and class? (nil? (:logseq.property.class/properties block)))
  662. [[:logseq.property.class/properties nil]]))
  663. remove-built-in-or-other-position-properties)]
  664. (cond
  665. (empty? full-properties)
  666. (when sidebar-properties?
  667. (rum/with-key (new-property block opts) (str id "-add-property")))
  668. :else
  669. (let [remove-properties #{:logseq.property/icon :logseq.property/query}
  670. properties' (remove (fn [[k _v]] (contains? remove-properties k)) full-properties)
  671. properties'' (->> properties'
  672. (remove (fn [[k _v]] (= k :logseq.property.class/properties))))
  673. page? (entity-util/page? block)]
  674. [:div.ls-properties-area
  675. {:id id
  676. :class (util/classnames [{:ls-page-properties page?}])
  677. :tab-index 0}
  678. [:<>
  679. (properties-section block properties'' opts)
  680. (when (and page? (not class?))
  681. (rum/with-key (new-property block opts) (str id "-add-property")))]
  682. (when class?
  683. (let [properties (->> (:logseq.property.class/properties block)
  684. (map (fn [e] [(:db/ident e)])))
  685. opts' (assoc opts :class-schema? true)]
  686. [:<>
  687. [:div.mt-2
  688. [:div.text-sm.text-muted-foreground.mb-2 {:style {:margin-left 10}}
  689. "Tag Properties:"]
  690. [:div
  691. (properties-section block properties opts')
  692. (rum/with-key (new-property block opts') (str id "-class-add-property"))]]]))]))))