property.cljs 42 KB

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