property.cljs 42 KB

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