property.cljs 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859
  1. (ns frontend.components.property
  2. "Block properties management."
  3. (:require [clojure.set :as set]
  4. [clojure.string :as string]
  5. [frontend.components.property.value :as pv]
  6. [frontend.components.select :as select]
  7. [frontend.components.svg :as svg]
  8. [frontend.config :as config]
  9. [frontend.db :as db]
  10. [frontend.db.async :as db-async]
  11. [frontend.db-mixins :as db-mixins]
  12. [frontend.db.model :as model]
  13. [logseq.outliner.property :as outliner-property]
  14. [frontend.handler.db-based.property :as db-property-handler]
  15. [frontend.handler.notification :as notification]
  16. [frontend.handler.property :as property-handler]
  17. [frontend.handler.page :as page-handler]
  18. [frontend.modules.shortcut.core :as shortcut]
  19. [frontend.state :as state]
  20. [frontend.ui :as ui]
  21. [frontend.util :as util]
  22. [logseq.shui.ui :as shui]
  23. [logseq.db.frontend.property :as db-property]
  24. [logseq.db.frontend.property.type :as db-property-type]
  25. [rum.core :as rum]
  26. [frontend.handler.route :as route-handler]
  27. [frontend.components.icon :as icon-component]
  28. [frontend.components.dnd :as dnd]
  29. [frontend.components.property.closed-value :as closed-value]
  30. [frontend.components.property.util :as components-pu]
  31. [promesa.core :as p]
  32. [logseq.db :as ldb]
  33. [logseq.db.frontend.order :as db-order]
  34. [logseq.outliner.core :as outliner-core]
  35. [dommy.core :as d]))
  36. (defn- <create-class-if-not-exists!
  37. [value]
  38. (when (string? value)
  39. (let [page-name (string/trim value)]
  40. (when-not (string/blank? page-name)
  41. (p/let [page (page-handler/<create-class! page-name {:redirect? false
  42. :create-first-block? false})]
  43. (:block/uuid page))))))
  44. (rum/defc class-select
  45. [property {:keys [multiple-choices? disabled?]
  46. :or {multiple-choices? true}}]
  47. (let [schema-classes (:property/schema.classes property)]
  48. [:div.flex.flex-1.col-span-3
  49. (let [content-fn
  50. (fn [{:keys [id]}]
  51. (let [toggle-fn #(shui/popup-hide! id)
  52. classes (model/get-all-classes (state/get-current-repo))
  53. options (cond->> (map (fn [[name id]]
  54. {:label name :value id})
  55. classes)
  56. (= :template (get-in property [:block/schema :type]))
  57. (remove (fn [[name _id]] (= name "Root class"))))
  58. opts {:items options
  59. :input-default-placeholder (if multiple-choices? "Choose classes" "Choose class")
  60. :dropdown? false
  61. :close-modal? false
  62. :multiple-choices? multiple-choices?
  63. :selected-choices (map :block/uuid schema-classes)
  64. :extract-fn :label
  65. :extract-chosen-fn :value
  66. :show-new-when-not-exact-match? true
  67. :input-opts {:on-blur toggle-fn
  68. :on-key-down
  69. (fn [e]
  70. (case (util/ekey e)
  71. "Escape"
  72. (do
  73. (util/stop e)
  74. (toggle-fn))
  75. nil))}
  76. :on-chosen (fn [value select?]
  77. (p/let [result (<create-class-if-not-exists! value)
  78. value' (or result value)
  79. tx-data [[(if select? :db/add :db/retract) (:db/id property) :property/schema.classes [:block/uuid value']]]]
  80. (db/transact! (state/get-current-repo) tx-data {:outliner-op :update-property})
  81. (when-not multiple-choices? (toggle-fn))))}]
  82. (select/select opts)))]
  83. [:div.flex.flex-1.cursor-pointer
  84. {:on-click (if disabled?
  85. (constantly nil)
  86. #(shui/popup-show! (.-target %) content-fn))}
  87. (if (seq schema-classes)
  88. [:div.flex.flex-1.flex-row.items-center.flex-wrap.gap-2
  89. (for [class schema-classes]
  90. [:a.text-sm (str "#" (:block/original-name class))])]
  91. (pv/property-empty-btn-value))])]))
  92. (defn- property-type-label
  93. [property-type]
  94. (case property-type
  95. :default
  96. "Text"
  97. ((comp string/capitalize name) property-type)))
  98. (defn- handle-delete-property!
  99. [block property & {:keys [class? class-schema?]}]
  100. (let [class? (or class? (some-> block :block/type (contains? "class")))]
  101. (when (or (not (and class? class-schema?))
  102. ;; Only ask for confirmation on class schema properties
  103. (js/confirm "Are you sure you want to delete this property?"))
  104. (let [repo (state/get-current-repo)]
  105. (if (and class? class-schema?)
  106. (db-property-handler/class-remove-property! (:db/id block) (:db/id property))
  107. (property-handler/remove-block-property! repo (:block/uuid block) (:db/ident property)))))))
  108. (defn- <add-property-from-dropdown
  109. "Adds an existing or new property from dropdown. Used from a block or page context.
  110. For pages, used to add both schema properties or properties for a page"
  111. [entity property-uuid-or-name schema {:keys [class-schema? page-configure?]}]
  112. (p/let [repo (state/get-current-repo)
  113. ;; Both conditions necessary so that a class can add its own page properties
  114. add-class-property? (and (contains? (:block/type entity) "class") page-configure? class-schema?)
  115. result (when (uuid? property-uuid-or-name)
  116. (db-async/<get-block repo property-uuid-or-name {:children? false}))
  117. ;; In block context result is in :block
  118. property (some-> (if (:block result) (:db/id (:block result)) (:db/id result))
  119. db/entity)]
  120. ;; existing property selected or entered
  121. (if property
  122. (do
  123. (when (and (not (get-in property [:block/schema :public?]))
  124. (ldb/built-in? property))
  125. (notification/show! "This is a private built-in property that can't be used." :error))
  126. property)
  127. ;; new property entered
  128. (if (db-property/valid-property-name? property-uuid-or-name)
  129. (if add-class-property?
  130. (p/let [result (db-property-handler/upsert-property! nil schema {:property-name property-uuid-or-name})
  131. property (db/entity (:db/id result))
  132. _ (pv/<add-property! entity (:db/ident property) "" {:class-schema? class-schema? :exit-edit? false})]
  133. property)
  134. (p/let [result (db-property-handler/upsert-property! nil schema {:property-name property-uuid-or-name})]
  135. (db/entity (:db/id result))))
  136. (notification/show! "This is an invalid property name. A property name cannot start with page reference characters '#' or '[['." :error)))))
  137. (rum/defc schema-type <
  138. shortcut/disable-all-shortcuts
  139. [property {:keys [*property-name *property-schema built-in? disabled?
  140. show-type-change-hints? block *show-new-property-config?
  141. default-open? page-configure? class-schema?]
  142. :as opts}]
  143. (let [property-name (or (and *property-name @*property-name) (:block/original-name property))
  144. property-schema (or (and *property-schema @*property-schema) (:block/schema property))
  145. schema-types (->> (concat db-property-type/user-built-in-property-types
  146. (when built-in?
  147. db-property-type/internal-built-in-property-types))
  148. (map (fn [type]
  149. {:label (property-type-label type)
  150. :value type})))]
  151. [:div {:class "flex items-center col-span-2"}
  152. (shui/select
  153. (cond->
  154. {:default-open (boolean default-open?)
  155. :disabled disabled?
  156. :on-value-change
  157. (fn [v]
  158. (let [type (keyword (string/lower-case v))
  159. update-schema-fn (apply comp
  160. #(assoc % :type type)
  161. (keep
  162. (fn [attr]
  163. (when-not (db-property-type/property-type-allows-schema-attribute? type attr)
  164. #(dissoc % attr)))
  165. [:cardinality :position]))]
  166. (when *property-schema
  167. (swap! *property-schema update-schema-fn))
  168. (let [schema (or (and *property-schema @*property-schema)
  169. (update-schema-fn property-schema))]
  170. (p/let [property' (when block (<add-property-from-dropdown block property-name schema opts))
  171. property (or property' property)
  172. add-class-property? (and (contains? (:block/type block) "class") page-configure? class-schema?)]
  173. (p/do!
  174. (when *show-new-property-config? (reset! *show-new-property-config? false))
  175. (components-pu/update-property! property property-name schema)
  176. (cond
  177. add-class-property?
  178. (shui/dialog-close!)
  179. (and block (= type :default)
  180. (not (seq (:property/closed-values property))))
  181. (pv/<create-new-block! block property "")))))))}
  182. ;; only set when in property configure modal
  183. (and *property-name (:type property-schema))
  184. (assoc :default-value (name (:type property-schema))))
  185. (shui/select-trigger
  186. {:class "!px-2 !py-0 !h-8"}
  187. (shui/select-value
  188. {:placeholder "Select a schema type"}))
  189. (shui/select-content
  190. (shui/select-group
  191. (for [{:keys [label value disabled]} schema-types]
  192. (shui/select-item {:value value :disabled disabled} label)))))
  193. (when show-type-change-hints?
  194. (ui/tippy {:html "Changing the property type clears some property configurations."
  195. :class "tippy-hover ml-2"
  196. :interactive true
  197. :disabled false}
  198. (svg/info)))]))
  199. (rum/defcs ^:large-vars/cleanup-todo property-config
  200. "All changes to a property must update the db and the *property-schema. Failure to do
  201. so can result in data loss"
  202. <
  203. shortcut/disable-all-shortcuts
  204. rum/reactive
  205. db-mixins/query
  206. (rum/local nil ::property-name)
  207. (rum/local nil ::property-schema)
  208. {:init (fn [state]
  209. (let [*values (atom :loading)]
  210. (p/let [result (db-async/<get-block-property-values (state/get-current-repo)
  211. (:db/ident (first (:rum/args state))))]
  212. (reset! *values result))
  213. (assoc state ::values *values)))
  214. :will-mount (fn [state]
  215. (let [[property _opts] (:rum/args state)]
  216. (reset! (::property-name state) (:block/original-name property))
  217. (reset! (::property-schema state) (:block/schema property))
  218. (state/set-state! :editor/property-configure? true)
  219. state))
  220. :will-unmount (fn [state]
  221. (util/schedule #(state/set-state! :editor/property-configure? false))
  222. (when-let [*show-property-config? (:*show-new-property-config? (last (:rum/args state)))]
  223. (reset! *show-property-config? false))
  224. state)}
  225. [state property {:keys [add-new-property?] :as opts}]
  226. (let [values (rum/react (::values state))]
  227. (when-not (= :loading values)
  228. (let [*property-name (::property-name state)
  229. *property-schema (::property-schema state)
  230. property (db/sub-block (:db/id property))
  231. built-in? (ldb/built-in? property)
  232. disabled? (or built-in? config/publishing?)
  233. property-type (get-in property [:block/schema :type])
  234. save-property-fn (fn [] (components-pu/update-property! property @*property-name @*property-schema))
  235. enable-closed-values? (contains? db-property-type/closed-value-property-types (or property-type :default))]
  236. [:div.property-configure.flex.flex-1.flex-col
  237. [:div.grid.gap-2.p-1
  238. [:div.grid.grid-cols-4.gap-1.items-center.leading-8
  239. [:label.col-span-1 "Name:"]
  240. (shui/input
  241. {:class "col-span-2 !px-2 !py-0 !h-8"
  242. :auto-focus (not add-new-property?)
  243. :on-change #(reset! *property-name (util/evalue %))
  244. :on-blur save-property-fn
  245. :on-key-press (fn [e]
  246. (when (= "Enter" (util/ekey e))
  247. (save-property-fn)))
  248. :disabled disabled?
  249. :default-value @*property-name})]
  250. [:div.grid.grid-cols-4.gap-1.items-center.leading-8
  251. [:label.col-span-1 "Icon:"]
  252. (let [icon-value (:logseq.property/icon property)]
  253. [:div.col-span-3.flex.flex-row.items-center.gap-2
  254. (icon-component/icon-picker icon-value
  255. {:on-chosen (fn [_e icon]
  256. (db-property-handler/upsert-property!
  257. (:db/ident property)
  258. (:block/schema property)
  259. {:properties {:logseq.property/icon icon}}))})
  260. (when icon-value
  261. [:a.fade-link.flex {:on-click (fn [_e]
  262. (db-property-handler/remove-block-property!
  263. (:db/ident property)
  264. :logseq.property/icon))
  265. :title "Delete this icon"}
  266. (ui/icon "X")])])]
  267. [:div.grid.grid-cols-4.gap-1.items-center.leading-8
  268. [:label.col-span-1 "Schema type:"]
  269. (if (or (ldb/built-in? property)
  270. (and property-type (seq values)))
  271. [:div.flex.items-center.col-span-2
  272. (property-type-label property-type)
  273. (ui/tippy {:html "The type of this property is locked once you start using it. This is to make sure all your existing information stays correct if the property type is changed later. To unlock, all uses of a property must be deleted."
  274. :class "tippy-hover ml-2"
  275. :interactive true
  276. :disabled false}
  277. (svg/help-circle))]
  278. (schema-type property {:*property-name *property-name
  279. :*property-schema *property-schema
  280. :built-in? built-in?
  281. :disabled? disabled?
  282. :show-type-change-hints? true}))]
  283. (when (db-property-type/property-type-allows-schema-attribute? (:type @*property-schema) :classes)
  284. (case (:type @*property-schema)
  285. ;; Question: 1. should we still support classes for `page` type?
  286. ;; 2. flexible query instead of classes? e.g. find all papers are related to either Clojure or OCaml `(and (tag :paper) (or (tag :clojure) (tag :ocaml)))`
  287. :object
  288. (when (empty? (:property/closed-values property))
  289. [:div.grid.grid-cols-4.gap-1.items-center.leading-8
  290. [:label "Specify classes:"]
  291. (class-select property (assoc opts :disabled? disabled?))])
  292. :template
  293. [:div.grid.grid-cols-4.gap-1.items-center.leading-8
  294. [:label "Specify template:"]
  295. (class-select property (assoc opts
  296. :multiple-choices? false
  297. :disabled? disabled?))]
  298. nil))
  299. (when (db-property-type/property-type-allows-schema-attribute? (:type @*property-schema) :cardinality)
  300. [:div.grid.grid-cols-4.gap-1.items-center.leading-8
  301. [:label "Multiple values:"]
  302. (let [many? (db-property/many? property)]
  303. (shui/checkbox
  304. {:checked many?
  305. :disabled disabled?
  306. :on-checked-change (fn []
  307. (swap! *property-schema assoc :cardinality (if many? :one :many))
  308. (save-property-fn))}))])
  309. (when (and enable-closed-values? (empty? (:property/schema.classes property)))
  310. [:div.grid.grid-cols-4.gap-1.items-start.leading-8
  311. [:label.col-span-1 "Available choices:"]
  312. [:div.col-span-3
  313. (closed-value/choices property opts)]])
  314. (when (db-property-type/property-type-allows-schema-attribute? (:type @*property-schema) :position)
  315. (let [position (:position @*property-schema)
  316. choices (map
  317. (fn [item]
  318. (assoc item :selected
  319. (or (and position (= (:value item) position))
  320. (and (nil? position) (= (:value item) :properties)))))
  321. [{:label "Block properties"
  322. :value :properties}
  323. {:label "Beginning of the block"
  324. :value :block-left}
  325. {:label "Ending of the block"
  326. :value :block-right}
  327. {:label "Below the block"
  328. :value :block-below}])]
  329. [:div.grid.grid-cols-4.gap-1.items-center.leading-8
  330. [:label.col-span-1 "UI position:"]
  331. [:div.col-span-2
  332. (shui/select
  333. (cond-> {:disabled config/publishing?
  334. :on-value-change (fn [v]
  335. (swap! *property-schema assoc :position (keyword v))
  336. (save-property-fn))}
  337. (keyword? position)
  338. (assoc :default-value position))
  339. (shui/select-trigger
  340. {:class "!px-2 !py-0 !h-8"}
  341. (shui/select-value
  342. {:placeholder "Select a position mode"}))
  343. (shui/select-content
  344. (shui/select-group
  345. (for [{:keys [label value]} choices]
  346. (shui/select-item {:value value} label)))))]]))
  347. (let [hide? (:hide? @*property-schema)]
  348. [:div.grid.grid-cols-4.gap-1.items-center.leading-8
  349. [:label "Hide by default:"]
  350. (shui/checkbox
  351. {:checked hide?
  352. :disabled config/publishing?
  353. :on-checked-change (fn []
  354. (swap! *property-schema assoc :hide? (not hide?))
  355. (save-property-fn))})])
  356. (let [description (or (:description @*property-schema) "")]
  357. [:div.grid.grid-cols-4.gap-1.items-start.leading-8
  358. [:label "Description:"]
  359. [:div.col-span-3
  360. [:div.mt-1
  361. (shui/textarea
  362. {:on-change (fn [e]
  363. (swap! *property-schema assoc :description (util/evalue e)))
  364. :on-blur save-property-fn
  365. :disabled disabled?
  366. :default-value description})]]])]]))))
  367. (rum/defc property-select
  368. [exclude-properties on-chosen input-opts]
  369. (let [[properties set-properties!] (rum/use-state nil)
  370. [excluded-properties set-excluded-properties!] (rum/use-state nil)]
  371. (rum/use-effect!
  372. (fn []
  373. (p/let [properties (db-async/<db-based-get-all-properties (state/get-current-repo))]
  374. (set-properties! (remove exclude-properties properties))
  375. (set-excluded-properties! (->> properties
  376. (filter exclude-properties)
  377. (map :block/original-name)
  378. set))))
  379. [])
  380. [:div.ls-property-add.flex.flex-row.items-center.property-key
  381. [:div.ls-property-key
  382. (select/select {:items (map (fn [x]
  383. {:label (:block/original-name x)
  384. :value (:block/uuid x)}) properties)
  385. :extract-fn :label
  386. :dropdown? false
  387. :close-modal? false
  388. :new-case-sensitive? true
  389. :show-new-when-not-exact-match? true
  390. :exact-match-exclude-items (fn [s] (contains? excluded-properties s))
  391. :input-default-placeholder "Add or change property"
  392. :on-chosen on-chosen
  393. :input-opts input-opts})]]))
  394. (rum/defc property-icon
  395. [property property-type]
  396. (let [type (or (get-in property [:block/schema :type] property-type) :default)
  397. icon (case type
  398. :number "hash"
  399. :date "calendar"
  400. :checkbox "checkbox"
  401. :url "link"
  402. :page "file"
  403. ;; FIXME: upgrade tabler icons
  404. :object "topology-star"
  405. :template "template"
  406. "letter-t")]
  407. (ui/icon icon {:class "opacity-50"
  408. :size 15})))
  409. (rum/defcs property-input < rum/reactive
  410. (rum/local false ::show-new-property-config?)
  411. (rum/local {} ::property-schema)
  412. {:will-unmount (fn [state]
  413. (let [args (:rum/args state)
  414. *property-key (second args)
  415. {:keys [original-block edit-original-block]} (last args)]
  416. (when *property-key (reset! *property-key nil))
  417. (when (and original-block edit-original-block
  418. (not= (:db/id original-block) (:db/id (state/get-edit-block)))) ; new block created
  419. (edit-original-block)))
  420. state)}
  421. shortcut/disable-all-shortcuts
  422. [state block *property-key {:keys [class-schema? page? page-configure?]
  423. :as opts}]
  424. (let [*show-new-property-config? (::show-new-property-config? state)
  425. *property-schema (::property-schema state)
  426. existing-tag-alias (->> db-property/db-attribute-properties
  427. (map db-property/built-in-properties)
  428. (keep #(when (get block (:attribute %)) (:original-name %)))
  429. set)
  430. exclude-properties (fn [m]
  431. (or (and (not page?) (contains? existing-tag-alias (:block/original-name m)))
  432. ;; Filters out properties from being in wrong :view-context
  433. (and (not page?) (= :page (get-in m [:block/schema :view-context])))
  434. (and page? (= :block (get-in m [:block/schema :view-context])))))
  435. property-key (rum/react *property-key)]
  436. [:div.ls-property-input.flex.flex-1.flex-row.items-center.flex-wrap.gap-1
  437. (if property-key
  438. (let [property (db/get-case-page @*property-key)]
  439. [:div.ls-property-add.grid.grid-cols-5.gap-1.flex.flex-1.flex-row.items-center
  440. [:div.flex.flex-row.items-center.col-span-2.property-key.gap-1
  441. (property-icon property (:type @*property-schema))
  442. [:div @*property-key]]
  443. [:div.col-span-3.flex.flex-row {:on-pointer-down (fn [e] (util/stop-propagation e))}
  444. (if @*show-new-property-config?
  445. (schema-type property (merge opts
  446. {:*property-name *property-key
  447. :*property-schema *property-schema
  448. :default-open? true
  449. :block block
  450. :*show-new-property-config? *show-new-property-config?}))
  451. (when (and property (not class-schema?))
  452. (pv/property-value block property (get block (:db/ident property)) (assoc opts :editing? true))))]])
  453. (let [on-chosen (fn [{:keys [value label]}]
  454. (reset! *property-key (if (uuid? value) label value))
  455. (let [property (when (uuid? value) (db/entity [:block/uuid value]))]
  456. (when (and *show-new-property-config? (not property))
  457. (reset! *show-new-property-config? true))
  458. (when property
  459. (let [add-class-property? (and (contains? (:block/type block) "class") class-schema?)
  460. type (get-in property [:block/schema :type])]
  461. (when property
  462. (cond
  463. add-class-property?
  464. (p/do!
  465. (pv/<add-property! block (:db/ident property) "" {:class-schema? class-schema?
  466. :exit-edit? page-configure?})
  467. (shui/dialog-close!))
  468. (and (= :default type)
  469. (not (seq (:property/closed-values property))))
  470. (p/do!
  471. (pv/<create-new-block! block property "")
  472. (shui/dialog-close!))
  473. (or (not= :default type)
  474. (and (= :default type) (seq (:property/closed-values property))))
  475. (p/do!
  476. (reset! *show-new-property-config? false))))))))
  477. input-opts {}]
  478. (property-select exclude-properties on-chosen input-opts)))]))
  479. (rum/defcs new-property < rum/reactive
  480. [state block opts]
  481. (when (and (:page-configure? opts) (not config/publishing?))
  482. [:div.ls-new-property
  483. [:a.fade-link.flex.add-property
  484. {:on-click (fn []
  485. (state/pub-event! [:editor/new-property (merge opts {:block block})]))}
  486. [:div.flex.flex-row.items-center
  487. (ui/icon "plus" {:size 15})
  488. [:div.ml-1.text-sm "Add property"]]]]))
  489. (defn- property-collapsed?
  490. [block property]
  491. (boolean?
  492. (some (fn [p] (= (:db/id property) (:db/id p)))
  493. (:block/collapsed-properties block))))
  494. (rum/defcs property-key <
  495. (rum/local false ::hover?)
  496. [state block property {:keys [class-schema? page-cp inline-text other-position?]}]
  497. (let [*hover? (::hover? state)
  498. icon (:logseq.property/icon property)
  499. property-name (:block/original-name property)]
  500. [:div.flex.flex-row.items-center.gap-1
  501. {:on-mouse-over #(reset! *hover? true)
  502. :on-mouse-leave #(reset! *hover? false)
  503. :on-context-menu (fn [^js e]
  504. (util/stop e)
  505. (shui/popup-show! e
  506. [(shui/dropdown-menu-item
  507. {:on-click (fn []
  508. (when-let [schema (some-> property :block/schema)]
  509. (components-pu/update-property! property property-name (assoc schema :hide? true))
  510. (shui/popup-hide!)))}
  511. "Hide property")
  512. (when-not (ldb/built-in-class-property? block property)
  513. (shui/dropdown-menu-item
  514. {:on-click (fn []
  515. (handle-delete-property! block property {:class-schema? class-schema?})
  516. (shui/popup-hide!))}
  517. [:span.w-full.text-red-rx-07.hover:text-red-rx-09
  518. "Delete property"]))]
  519. {:as-dropdown? true
  520. :content-props {:class "w-48"}}))}
  521. ;; icon picker
  522. (when-not other-position?
  523. (let [content-fn (fn [{:keys [id]}]
  524. (icon-component/icon-search
  525. {:on-chosen
  526. (fn [_e icon]
  527. (when icon
  528. (p/let [_ (db-property-handler/upsert-property! (:db/ident property)
  529. (:block/schema property)
  530. {:properties {:logseq.property/icon icon}})]
  531. (shui/popup-hide! id))))}))]
  532. (shui/trigger-as :button
  533. (-> (when-not config/publishing?
  534. {:on-click #(shui/popup-show! (.-target %) content-fn {:as-dropdown? true :auto-focus? true})})
  535. (assoc :class "flex items-center"))
  536. (if icon
  537. (icon-component/icon icon {:size 15})
  538. (property-icon property nil)))))
  539. (if config/publishing?
  540. [:a.property-k.flex.select-none.jtrigger
  541. {:on-click #(route-handler/redirect-to-page! (:block/uuid property))}
  542. (:block/original-name property)]
  543. (shui/trigger-as :a
  544. {:tabIndex 0
  545. :title (str "Configure property: " (:block/original-name property))
  546. :class "property-k flex select-none jtrigger"
  547. :on-pointer-down (fn [^js e]
  548. (when (util/meta-key? e)
  549. (route-handler/redirect-to-page! (:block/uuid property))
  550. (.preventDefault e)))
  551. :on-click (fn [^js e]
  552. (shui/popup-show!
  553. (.-target e)
  554. (fn [{:keys [id]}]
  555. [:div.p-2
  556. [:h2.text-lg.font-medium.mb-2.p-1 "Configure property"]
  557. [:span.close.absolute.right-2.top-2
  558. (shui/button
  559. {:variant :ghost :size :sm :class "!w-4 !h-6"
  560. :on-click #(shui/popup-hide! id)}
  561. (shui/tabler-icon "x" {:size 16}))]
  562. (property-config property
  563. {:inline-text inline-text
  564. :page-cp page-cp})])
  565. {:content-props {:class "property-configure-popup-content"
  566. :collisionPadding {:bottom 10 :top 10}
  567. :avoidCollisions true
  568. :align "start"}
  569. :auto-side? true
  570. :auto-focus? true}))}
  571. (:block/original-name property)))]))
  572. (defn- resolve-linked-block-if-exists
  573. "Properties will be updated for the linked page instead of the refed block.
  574. For example, the block below has a reference to the page \"How to solve it\",
  575. we'd like the properties of the class \"book\" (e.g. Authors, Published year)
  576. to be assigned for the page `How to solve it` instead of the referenced block.
  577. Block:
  578. - [[How to solve it]] #book
  579. "
  580. [block]
  581. (if-let [linked-block (:block/link block)]
  582. (db/sub-block (:db/id linked-block))
  583. (db/sub-block (:db/id block))))
  584. (rum/defc property-cp <
  585. rum/reactive
  586. db-mixins/query
  587. [block k v {:keys [inline-text page-cp] :as opts}]
  588. (when (keyword? k)
  589. (when-let [property (db/sub-block (:db/id (db/entity k)))]
  590. (let [type (get-in property [:block/schema :type] :default)
  591. closed-values? (seq (:property/closed-values property))
  592. block? (and v
  593. (not closed-values?)
  594. (:block/page v)
  595. (contains? #{:default :template} type))
  596. collapsed? (when block? (property-collapsed? block property))
  597. date? (= type :date)
  598. checkbox? (= type :checkbox)]
  599. [:div {:class (cond
  600. (or date? checkbox?)
  601. "property-pair items-center"
  602. :else
  603. "property-pair items-start")}
  604. [:div.property-key.col-span-2
  605. (property-key block property (assoc (select-keys opts [:class-schema?])
  606. :block? block?
  607. :collapsed? collapsed?
  608. :inline-text inline-text
  609. :page-cp page-cp))]
  610. [:div.property-value-container.col-span-3.flex.flex-row.gap-1.items-center
  611. (when-not block? [:div.opacity-30 {:style {:margin-left 5}}
  612. [:span.bullet-container.cursor [:span.bullet]]])
  613. [:div.flex.flex-1
  614. (if (and (:class-schema? opts) (:page-configure? opts))
  615. [:div.property-description.text-sm.opacity-70
  616. (inline-text {} :markdown (get-in property [:block/schema :description]))]
  617. (when-not collapsed?
  618. [:div.property-value.flex.flex-1
  619. (pv/property-value block property v opts)]))]]]))))
  620. (rum/defcs ordered-properties < rum/reactive
  621. {:init (fn [state]
  622. (assoc state ::properties-order (atom (mapv first (second (:rum/args state))))))
  623. :should-update (fn [old-state new-state]
  624. (let [[_ p1 opts1] (:rum/args old-state)
  625. [_ p2 opts2] (:rum/args new-state)
  626. p1-keys (map first p1)
  627. p1-set (set p1-keys)
  628. p1-m (zipmap (map first p1) (map second p1))
  629. p2-m (zipmap (map first p2) (map second p2))
  630. p2-set (set (map first p2))]
  631. (when-not (= p1-set p2-set)
  632. (reset! (::properties-order new-state) (mapv first p2)))
  633. (not= [p1-set (map p1-m p1-keys) opts1] [p2-set (map p2-m p1-keys) opts2])))}
  634. [state block properties opts]
  635. (let [*properties-order (::properties-order state)
  636. properties-order (rum/react *properties-order)
  637. m (zipmap (map first properties) (map second properties))
  638. properties (mapv (fn [k] [k (get m k)]) properties-order)
  639. choices (map (fn [[k v]]
  640. {:id (subs (str k) 1)
  641. :value k
  642. :content (property-cp block k v opts)}) properties)]
  643. (dnd/items choices
  644. {:on-drag-end (fn [properties-order {:keys [active-id over-id direction]}]
  645. (let [move-down? (= direction :down)
  646. over (db/entity (keyword over-id))
  647. active (db/entity (keyword active-id))
  648. over-order (:block/order over)
  649. new-order (if move-down?
  650. (let [next-order (db-order/get-next-order (db/get-db) nil (:db/id over))]
  651. (db-order/gen-key over-order next-order))
  652. (let [prev-order (db-order/get-prev-order (db/get-db) nil (:db/id over))]
  653. (db-order/gen-key prev-order over-order)))]
  654. ;; Reset *properties-order without waiting for `db/transact!` so that the UI will not be
  655. ;; converted back to the old order and then the new order.
  656. (reset! *properties-order properties-order)
  657. (db/transact! (state/get-current-repo)
  658. [{:db/id (:db/id active)
  659. :block/order new-order}
  660. (outliner-core/block-with-updated-at
  661. {:db/id (:db/id block)})]
  662. {:outliner-op :save-block})))})))
  663. (rum/defc properties-section < rum/reactive db-mixins/query
  664. [block properties opts]
  665. (when (seq properties)
  666. ;; Sort properties by :block/order
  667. (let [properties' (sort-by (fn [[k _v]]
  668. (:block/order (db/entity k))) properties)]
  669. (ordered-properties block properties' opts))))
  670. (defn- async-load-classes!
  671. [block]
  672. (let [repo (state/get-current-repo)
  673. classes (concat (:block/tags block) (outliner-property/get-class-parents (:block/tags block)))]
  674. (doseq [class classes]
  675. (db-async/<get-block repo (:db/id class) :children? false))
  676. classes))
  677. ;; TODO: Remove :page-configure? as it only ever seems to be set to true
  678. (rum/defcs ^:large-vars/cleanup-todo properties-area < rum/reactive db-mixins/query
  679. {:init (fn [state]
  680. (let [target-block (first (:rum/args state))
  681. block (resolve-linked-block-if-exists target-block)]
  682. (assoc state
  683. ::id (str (random-uuid))
  684. ::block block
  685. ::classes (async-load-classes! block))))
  686. :will-remount (fn [state]
  687. (let [block (db/entity (:db/id (::block state)))]
  688. (assoc state ::classes (async-load-classes! block))))}
  689. [state _target-block {:keys [page-configure? class-schema?] :as opts}]
  690. (let [id (::id state)
  691. block (db/sub-block (:db/id (::block state)))
  692. _ (doseq [class (::classes state)]
  693. (db/sub-block (:db/id class)))
  694. page? (db/page? block)
  695. block-properties (:block/properties block)
  696. properties (if (and class-schema? page-configure?)
  697. (->> (db-property/get-class-ordered-properties block)
  698. (map :db/ident)
  699. (map #(vector % %)))
  700. block-properties)
  701. remove-built-in-or-other-position-properties
  702. (fn [properties]
  703. (remove (fn [property]
  704. (let [id (if (vector? property) (first property) property)]
  705. (or
  706. (when-not page? (= id :block/tags))
  707. (when-let [ent (db/entity id)]
  708. (or
  709. ;; built-in
  710. (and (not (get-in ent [:block/schema :public?]))
  711. (ldb/built-in? ent))
  712. ;; other position
  713. (when-not (or (and (:sidebar? opts) (= (:id opts) (str (:block/uuid block))))
  714. (ldb/page? block))
  715. (outliner-property/property-with-other-position? ent)))))))
  716. properties))
  717. {:keys [classes all-classes classes-properties]} (outliner-property/get-block-classes-properties (db/get-db) (:db/id block))
  718. one-class? (= 1 (count classes))
  719. classes-properties-set (set classes-properties)
  720. block-own-properties (->> (concat (when (seq (:block/alias block))
  721. [[:block/alias (:block/alias block)]])
  722. (seq properties))
  723. (remove (fn [[id _]] (classes-properties-set id)))
  724. remove-built-in-or-other-position-properties)
  725. root-block? (= (:id opts) (str (:block/uuid block)))
  726. ;; This section produces own-properties and full-hidden-properties
  727. hide-with-property-id (fn [property-id]
  728. (cond
  729. (or root-block? page-configure?)
  730. false
  731. :else
  732. (boolean (:hide? (:block/schema (db/entity property-id))))))
  733. property-hide-f (cond
  734. config/publishing?
  735. ;; Publishing is read only so hide all blank properties as they
  736. ;; won't be edited and distract from properties that have values
  737. (fn [[property-id property-value]]
  738. (or (nil? property-value)
  739. (hide-with-property-id property-id)))
  740. (:ui/hide-empty-properties? (state/get-config))
  741. (fn [[property-id property-value]]
  742. ;; User's selection takes precedence over config
  743. (if (contains? (:block/schema (db/entity property-id)) :hide?)
  744. (hide-with-property-id property-id)
  745. (nil? property-value)))
  746. :else
  747. (comp hide-with-property-id first))
  748. {_block-hidden-properties true
  749. block-own-properties' false} (group-by property-hide-f block-own-properties)
  750. {_class-hidden-properties true
  751. class-own-properties false} (group-by property-hide-f
  752. (map (fn [id] [id (get block-properties id)]) classes-properties))
  753. own-properties (->>
  754. (if one-class?
  755. (->> (concat block-own-properties' class-own-properties)
  756. remove-built-in-or-other-position-properties)
  757. block-own-properties'))
  758. class->properties (loop [classes all-classes
  759. properties #{}
  760. result []]
  761. (if-let [class (first classes)]
  762. (let [cur-properties (->> (db-property/get-class-ordered-properties class)
  763. (map :db/ident)
  764. (remove properties)
  765. (remove hide-with-property-id)
  766. remove-built-in-or-other-position-properties)]
  767. (recur (rest classes)
  768. (set/union properties (set cur-properties))
  769. (if (seq cur-properties)
  770. (conj result [class cur-properties])
  771. result)))
  772. result))]
  773. (when-not (and (empty? block-own-properties')
  774. (empty? class->properties)
  775. (not (:page-configure? opts)))
  776. [:div.ls-properties-area
  777. (cond-> {:id id}
  778. class-schema?
  779. (assoc :class "class-properties")
  780. true (assoc :tab-index 0
  781. :on-key-up #(when-let [block (and (= "Escape" (.-key %))
  782. (.closest (.-target %) "[blockid]"))]
  783. (let [target (.-target %)]
  784. (when-not (d/has-class? target "ls-popup-closed")
  785. (state/set-selection-blocks! [block])
  786. (some-> js/document.activeElement (.blur)))
  787. (d/remove-class! target "ls-popup-closed")))))
  788. (let [own-properties' (cond
  789. (and page? page-configure?)
  790. (concat [[:block/tags (:block/tags block)]
  791. [:logseq.property/icon (:logseq.property/icon block)]]
  792. (remove (fn [[k _v]] (contains? #{:block/tags :logseq.property/icon} k)) own-properties))
  793. page?
  794. (remove (fn [[k _v]] (contains? #{:logseq.property/icon} k)) own-properties)
  795. :else
  796. own-properties)]
  797. (properties-section block (if class-schema? properties own-properties') opts))
  798. (rum/with-key (new-property block opts) (str id "-add-property"))
  799. (when (and (seq class->properties) (not one-class?))
  800. (let [class-properties-col (keep
  801. (fn [[class class-properties]]
  802. (let [properties (->> class-properties
  803. (map (fn [id] [id (get block-properties id)])))]
  804. (when (seq properties)
  805. [class properties])))
  806. class->properties)]
  807. (when (seq class-properties-col)
  808. (let [page-cp (:page-cp opts)]
  809. [:div.parent-properties.flex.flex-1.flex-col.gap-1
  810. (for [[class id-properties] class-properties-col]
  811. (when (seq id-properties)
  812. [:div
  813. (when page-cp
  814. [:span.text-sm.ml-4 (page-cp {} class)])
  815. (properties-section block id-properties opts)]))]))))])))