property.cljs 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814
  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.modules.shortcut.core :as shortcut]
  18. [frontend.state :as state]
  19. [frontend.ui :as ui]
  20. [frontend.util :as util]
  21. [logseq.shui.ui :as shui]
  22. [logseq.db.frontend.property :as db-property]
  23. [logseq.db.frontend.property.type :as db-property-type]
  24. [rum.core :as rum]
  25. [frontend.handler.route :as route-handler]
  26. [frontend.components.icon :as icon-component]
  27. [frontend.components.dnd :as dnd]
  28. [frontend.components.property.closed-value :as closed-value]
  29. [frontend.components.property.util :as components-pu]
  30. [promesa.core :as p]
  31. [logseq.db :as ldb]))
  32. (defn- <create-class-if-not-exists!
  33. [value]
  34. (when (string? value)
  35. (let [page-name (string/trim value)]
  36. (when-not (string/blank? page-name)
  37. (p/let [page (page-handler/<create! page-name {:redirect? false
  38. :create-first-block? false
  39. :class? true})]
  40. (:block/uuid page))))))
  41. (rum/defc class-select
  42. [*property-schema schema-classes {:keys [multiple-choices? save-property-fn disabled?]
  43. :or {multiple-choices? true}}]
  44. [:div.flex.flex-1.col-span-3
  45. (let [content-fn
  46. (fn [{:keys [id]}]
  47. (let [toggle-fn #(shui/popup-hide! id)
  48. classes (model/get-all-classes (state/get-current-repo))
  49. options (cond->> (map (fn [[name id]]
  50. {:label name :value id})
  51. classes)
  52. (not= :template (:type @*property-schema))
  53. (concat [{:label "Logseq Class" :value :logseq.class}]))
  54. opts (cond->
  55. {:items options
  56. :input-default-placeholder (if multiple-choices? "Choose classes" "Choose class")
  57. :dropdown? false
  58. :close-modal? false
  59. :multiple-choices? multiple-choices?
  60. :selected-choices schema-classes
  61. :extract-fn :label
  62. :extract-chosen-fn :value
  63. :show-new-when-not-exact-match? true
  64. :input-opts {:on-blur toggle-fn
  65. :on-key-down
  66. (fn [e]
  67. (case (util/ekey e)
  68. "Escape"
  69. (do
  70. (util/stop e)
  71. (toggle-fn))
  72. nil))}}
  73. multiple-choices?
  74. (assoc :on-apply (fn [choices]
  75. (p/let [choices' (p/all (map (fn [value]
  76. (p/let [result (<create-class-if-not-exists! value)]
  77. (or result value))) choices))
  78. _ (swap! *property-schema assoc :classes (set choices'))
  79. _ (save-property-fn)]
  80. (toggle-fn))))
  81. (not multiple-choices?)
  82. (assoc :on-chosen (fn [value]
  83. (p/let [result (<create-class-if-not-exists! value)
  84. value' (or result value)
  85. _ (swap! *property-schema assoc :classes #{value'})
  86. _ (save-property-fn)]
  87. (toggle-fn)))))]
  88. (select/select opts)))]
  89. [:div.flex.flex-1.cursor-pointer
  90. {:on-click (if disabled?
  91. (constantly nil)
  92. #(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? 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?
  130. db-property-type/internal-built-in-property-types))
  131. (map (fn [type]
  132. {:label (property-type-label type)
  133. :value type})))]
  134. [:div {:class (if in-block-container? "flex flex-1" "flex items-center col-span-2")}
  135. (shui/select
  136. (cond->
  137. {:default-open (boolean default-open?)
  138. :disabled disabled?
  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. (when *show-new-property-config?
  161. (reset! *show-new-property-config? false))
  162. (components-pu/update-property! property property-name schema)
  163. (when block
  164. (let [id (str "ls-property-" (:db/id block) "-" (:db/id property) "-editor")]
  165. (state/set-state! :editor/editing-property-value-id
  166. {id true}))
  167. (property-handler/set-block-property! repo (:block/uuid block)
  168. property-name
  169. (if (= type :default) "" :property/empty-placeholder)))))))}
  170. (shui/select-trigger
  171. {:class "!px-2 !py-0 !h-8"}
  172. (shui/select-value
  173. {:placeholder "Select a schema type"}))
  174. (shui/select-content
  175. (shui/select-group
  176. (for [{:keys [label value]} schema-types]
  177. (shui/select-item {:value value} label)))))
  178. (when show-type-change-hints?
  179. (ui/tippy {:html "Changing the property type clears some property configurations."
  180. :class "tippy-hover ml-2"
  181. :interactive true
  182. :disabled false}
  183. (svg/info))))]))
  184. (rum/defcs ^:large-vars/cleanup-todo property-config
  185. "All changes to a property must update the db and the *property-schema. Failure to do
  186. so can result in data loss"
  187. <
  188. shortcut/disable-all-shortcuts
  189. rum/reactive
  190. db-mixins/query
  191. (rum/local nil ::property-name)
  192. (rum/local nil ::property-schema)
  193. {:init (fn [state]
  194. (let [*values (atom :loading)]
  195. (p/let [result (db-async/<get-block-property-values (state/get-current-repo)
  196. (:db/ident (first (:rum/args state))))]
  197. (reset! *values result))
  198. (assoc state ::values *values)))
  199. :will-mount (fn [state]
  200. (let [[property _opts] (:rum/args state)]
  201. (reset! (::property-name state) (:block/original-name property))
  202. (reset! (::property-schema state) (:block/schema property))
  203. (state/set-state! :editor/property-configure? true)
  204. state))
  205. :will-unmount (fn [state]
  206. (util/schedule #(state/set-state! :editor/property-configure? false))
  207. (when-let [*show-property-config? (:*show-new-property-config? (last (:rum/args state)))]
  208. (reset! *show-property-config? false))
  209. state)}
  210. [state property {:keys [inline-text add-new-property?] :as opts}]
  211. (let [values (rum/react (::values state))]
  212. (when-not (= :loading values)
  213. (let [*property-name (::property-name state)
  214. *property-schema (::property-schema state)
  215. property (db/sub-block (:db/id property))
  216. built-in? (ldb/built-in? property)
  217. disabled? (or built-in? config/publishing?)
  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 (:logseq.property/icon property)]
  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. (db-property-handler/<update-property!
  242. (state/get-current-repo)
  243. (:db/ident property)
  244. {:properties {:logseq.property/icon icon}}))})
  245. (when icon-value
  246. [:a.fade-link.flex {:on-click (fn [_e]
  247. (db-property-handler/remove-block-property!
  248. (state/get-current-repo)
  249. (:db/ident property)
  250. :logseq.property/icon))
  251. :title "Delete this icon"}
  252. (ui/icon "X")])])]
  253. [:div.grid.grid-cols-4.gap-1.items-center.leading-8
  254. [:label.col-span-1 "Schema type:"]
  255. (if (or (ldb/built-in? property)
  256. (and property-type (seq values)))
  257. [:div.flex.items-center.col-span-2
  258. (property-type-label property-type)
  259. (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."
  260. :class "tippy-hover ml-2"
  261. :interactive true
  262. :disabled false}
  263. (svg/help-circle))]
  264. (schema-type property {:*property-name *property-name
  265. :*property-schema *property-schema
  266. :built-in? built-in?
  267. :disabled? disabled?
  268. :show-type-change-hints? true}))]
  269. (when (db-property-type/property-type-allows-schema-attribute? (:type @*property-schema) :cardinality)
  270. [:div.grid.grid-cols-4.gap-1.items-center.leading-8
  271. [:label "Multiple values:"]
  272. (let [many? (boolean (= :many (:cardinality @*property-schema)))]
  273. (shui/checkbox
  274. {:checked many?
  275. :disabled disabled?
  276. :on-checked-change (fn []
  277. (swap! *property-schema assoc :cardinality (if many? :one :many))
  278. (save-property-fn))}))])
  279. (when (db-property-type/property-type-allows-schema-attribute? (:type @*property-schema) :classes)
  280. (case (:type @*property-schema)
  281. :page
  282. (when (empty? (:values @*property-schema))
  283. [:div.grid.grid-cols-4.gap-1.items-center.leading-8
  284. [:label "Specify classes:"]
  285. (class-select *property-schema
  286. (:classes @*property-schema)
  287. (assoc opts
  288. :disabled? disabled?
  289. :save-property-fn save-property-fn))])
  290. :template
  291. [:div.grid.grid-cols-4.gap-1.items-center.leading-8
  292. [:label "Specify template:"]
  293. (class-select *property-schema (:classes @*property-schema)
  294. (assoc opts
  295. :multiple-choices? false
  296. :disabled? disabled?
  297. :save-property-fn save-property-fn))]
  298. nil))
  299. (when (and enable-closed-values? (empty? (:classes @*property-schema)))
  300. [:div.grid.grid-cols-4.gap-1.items-start.leading-8
  301. [:label.col-span-1 "Available choices:"]
  302. [:div.col-span-3
  303. (closed-value/choices property *property-name *property-schema opts)]])
  304. (when (and enable-closed-values?
  305. (db-property-type/property-type-allows-schema-attribute? (:type @*property-schema) :position)
  306. (seq (:values @*property-schema)))
  307. (let [position (:position @*property-schema)
  308. choices (map
  309. (fn [item]
  310. (assoc item :selected
  311. (or (and position (= (:value item) position))
  312. (and (nil? position) (= (:value item) "properties")))))
  313. [{:label "Block properties"
  314. :value "properties"}
  315. {:label "Beginning of the block"
  316. :value "block-beginning"}
  317. ;; {:label "Ending of the block"
  318. ;; :value "block-ending"}
  319. ])]
  320. [:div.grid.grid-cols-4.gap-1.items-center.leading-8
  321. [:label.col-span-1 "UI position:"]
  322. [:div.col-span-2
  323. (shui/select
  324. (cond-> {:disabled config/publishing?
  325. :on-value-change (fn [v]
  326. (swap! *property-schema assoc :position v)
  327. (save-property-fn))}
  328. (string? position)
  329. (assoc :default-value position))
  330. (shui/select-trigger
  331. {:class "!px-2 !py-0 !h-8"}
  332. (shui/select-value
  333. {:placeholder "Select a position mode"}))
  334. (shui/select-content
  335. (shui/select-group
  336. (for [{:keys [label value]} choices]
  337. (shui/select-item {:value value} label)))))]]))
  338. (let [hide? (:hide? @*property-schema)]
  339. [:div.grid.grid-cols-4.gap-1.items-center.leading-8
  340. [:label "Hide by default:"]
  341. (shui/checkbox
  342. {:checked hide?
  343. :disabled config/publishing?
  344. :on-checked-change (fn []
  345. (swap! *property-schema assoc :hide? (not hide?))
  346. (save-property-fn))})])
  347. (let [description (:description @*property-schema)]
  348. (when (or (not disabled?)
  349. (and disabled? (not (string/blank? description))))
  350. [:div.grid.grid-cols-4.gap-1.items-start.leading-8
  351. [:label "Description:"]
  352. [:div.col-span-3
  353. (if (and disabled? inline-text)
  354. (inline-text {} :markdown description)
  355. [:div.mt-1
  356. (shui/textarea
  357. {:on-change (fn [e]
  358. (swap! *property-schema assoc :description (util/evalue e)))
  359. :on-blur save-property-fn
  360. :disabled disabled?
  361. :default-value description})])]]))]]))))
  362. (defn- get-property-from-db [name]
  363. (when-not (string/blank? name)
  364. (db/entity (db-property/get-db-ident-from-name name))))
  365. (defn- add-property-from-dropdown
  366. "Adds an existing or new property from dropdown. Used from a block or page context.
  367. For pages, used to add both schema properties or properties for a page"
  368. [entity property-name {:keys [class-schema? page-configure?]}]
  369. (let [repo (state/get-current-repo)]
  370. ;; existing property selected or entered
  371. (if-let [property (get-property-from-db property-name)]
  372. (if (and (not (get-in property [:block/schema :public?]))
  373. (ldb/built-in? property))
  374. (do (notification/show! "This is a private built-in property that can't be used." :error)
  375. (pv/exit-edit-property))
  376. ;; Both conditions necessary so that a class can add its own page properties
  377. (when (and (contains? (:block/type entity) "class") class-schema?)
  378. (pv/<add-property! entity property-name "" {:class-schema? class-schema?
  379. ;; Only enter property names from sub-modal as inputting
  380. ;; property values is buggy in sub-modal
  381. :exit-edit? page-configure?})))
  382. ;; new property entered
  383. (if (db-property/valid-property-name? property-name)
  384. (if (and (contains? (:block/type entity) "class") page-configure?)
  385. (pv/<add-property! entity property-name "" {:class-schema? class-schema? :exit-edit? page-configure?})
  386. (p/do!
  387. (db-property-handler/upsert-property! repo nil {} {:property-name property-name})
  388. true))
  389. (do (notification/show! "This is an invalid property name. A property name cannot start with page reference characters '#' or '[['." :error)
  390. (pv/exit-edit-property))))))
  391. (rum/defc property-select
  392. [exclude-properties on-chosen input-opts]
  393. (let [[properties set-properties!] (rum/use-state nil)
  394. [excluded-properties set-excluded-properties!] (rum/use-state nil)]
  395. (rum/use-effect!
  396. (fn []
  397. (p/let [properties (db-async/<db-based-get-all-properties (state/get-current-repo))]
  398. (set-properties! (map :block/original-name (remove exclude-properties properties)))
  399. (set-excluded-properties! (->> properties
  400. (filter exclude-properties)
  401. ;; lower case b/c of case insensitive name lookups
  402. (map (comp string/lower-case :block/original-name))
  403. set))))
  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 (fn [s] (contains? excluded-properties (string/lower-case s)))
  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? in-block-container? page?]
  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 %)))
  429. (remove nil?)
  430. (set))
  431. existing-tag-alias (->> [:block/tags :block/alias]
  432. (map db-property/built-in-properties)
  433. (keep #(when (get entity (:attribute %)) (:original-name %)))
  434. set)
  435. exclude-property-names (set/union entity-properties existing-tag-alias)
  436. exclude-properties (fn [m]
  437. (or (contains? exclude-property-names (:block/original-name m))
  438. ;; Filters out properties from being in wrong :view-context
  439. (and in-block-container? (= :page (get-in m [:block/schema :view-context])))
  440. (and page? (= :block (get-in m [:block/schema :view-context])))))]
  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-pointer-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 {: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 (:logseq.property/icon property)
  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? 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-dropdown? 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. (when icon
  557. (p/let [_ (db-property-handler/<update-property! repo
  558. (:db/ident property)
  559. {:properties {:logseq.property/icon icon}})]
  560. (shui/popup-hide! id))))}))]
  561. (shui/trigger-as :button
  562. (-> (when-not config/publishing?
  563. {:on-click #(shui/popup-show! (.-target %) content-fn {:as-dropdown? true :auto-focus? true})})
  564. (assoc :class "flex items-center"))
  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.flex.select-none.jtrigger.pl-2
  571. {:on-click #(route-handler/redirect-to-page! (:block/name property))}
  572. (:block/original-name property)]
  573. (shui/trigger-as :a
  574. {:tabIndex 0
  575. :title (str "Configure property: " (:block/original-name property))
  576. :class "property-k flex select-none jtrigger pl-2"
  577. :on-pointer-down (fn [^js e]
  578. (when (util/meta-key? e)
  579. (route-handler/redirect-to-page! (:block/name property))
  580. (.preventDefault e)))
  581. :on-click (fn [^js e]
  582. (shui/popup-show!
  583. (.-target e)
  584. (fn [{:keys [id]}]
  585. [:div.p-2
  586. [:h2.text-lg.font-medium.mb-2.p-1 "Configure property"]
  587. [:span.close.absolute.right-2.top-2
  588. (shui/button
  589. {:variant :ghost :size :sm :class "!w-4 !h-6"
  590. :on-click #(shui/popup-hide! id)}
  591. (shui/tabler-icon "x" {:size 16}))]
  592. (property-config property
  593. {:inline-text inline-text
  594. :page-cp page-cp})])
  595. {:content-props {:class "property-configure-popup-content"
  596. :collisionPadding {:bottom 10 :top 10}
  597. :avoidCollisions true
  598. :align "start"}
  599. :auto-side? true
  600. :auto-focus? true}))}
  601. (:block/original-name property)))]))
  602. (defn- resolve-linked-block-if-exists
  603. "Properties will be updated for the linked page instead of the refed block.
  604. For example, the block below has a reference to the page \"How to solve it\",
  605. we'd like the properties of the class \"book\" (e.g. Authors, Published year)
  606. to be assigned for the page `How to solve it` instead of the referenced block.
  607. Block:
  608. - [[How to solve it]] #book
  609. "
  610. [block]
  611. (if-let [linked-block (:block/link block)]
  612. (db/sub-block (:db/id linked-block))
  613. (db/sub-block (:db/id block))))
  614. (rum/defc property-cp <
  615. rum/reactive
  616. db-mixins/query
  617. [block k v {:keys [inline-text page-cp] :as opts}]
  618. (when (keyword? k)
  619. (when-let [property (db/sub-block (:db/id (db/entity k)))]
  620. (let [type (get-in property [:block/schema :type] :default)
  621. closed-values? (seq (get-in property [:block/schema :values]))
  622. v-block (when (integer? v) (db/entity v))
  623. block? (and v-block
  624. (not closed-values?)
  625. (:block/page v-block)
  626. (contains? #{:default :template} type))
  627. collapsed? (when block? (property-collapsed? block property))
  628. date? (= type :date)
  629. checkbox? (= type :checkbox)]
  630. [:div {:class (cond
  631. (and block? (not closed-values?))
  632. "flex flex-1 flex-col gap-1 property-block"
  633. (or date? checkbox?)
  634. "property-pair items-center"
  635. :else
  636. "property-pair items-start")}
  637. [:div.property-key
  638. {:class "col-span-2"}
  639. (property-key block property (assoc (select-keys opts [:class-schema?])
  640. :block? block?
  641. :collapsed? collapsed?
  642. :inline-text inline-text
  643. :page-cp page-cp))]
  644. (if (and (:class-schema? opts) (:page-configure? opts))
  645. [:div.property-description.text-sm.opacity-70
  646. {:class "col-span-3"}
  647. (inline-text {} :markdown (get-in property [:block/schema :description]))]
  648. (when-not collapsed?
  649. [:div.property-value
  650. {:class (if block?
  651. "block-property-value"
  652. "col-span-3 inline-grid")}
  653. (pv/property-value block property v opts)]))]))))
  654. (rum/defc properties-section < rum/reactive db-mixins/query
  655. [block properties opts]
  656. (let [class? (:class-schema? opts)]
  657. (when (seq properties)
  658. (if class?
  659. (let [choices (map (fn [[k v]]
  660. {:id (str k)
  661. :value k
  662. :content (property-cp block k v opts)}) properties)]
  663. (dnd/items choices
  664. {:on-drag-end (fn [properties]
  665. (let [schema (assoc (:block/schema block)
  666. :properties properties)]
  667. (when (seq properties)
  668. (db-property-handler/class-set-schema! (state/get-current-repo) (:block/uuid block) schema))))}))
  669. (for [[k v] properties]
  670. (property-cp block k v opts))))))
  671. ;; TODO: Remove :page-configure? as it only ever seems to be set to true
  672. (rum/defcs ^:large-vars/cleanup-todo properties-area < rum/reactive
  673. {:init (fn [state]
  674. (assoc state ::id (str (random-uuid))))}
  675. [state target-block edit-input-id {:keys [in-block-container? page? page-configure? class-schema?] :as opts}]
  676. (let [id (::id state)
  677. block (resolve-linked-block-if-exists target-block)
  678. block-properties (:block/properties block)
  679. properties (if (and class-schema? page-configure?)
  680. (->> (db-property/get-class-ordered-properties block)
  681. (map #(vector % %)))
  682. (sort-by first block-properties))
  683. alias (set (map :db/id (:block/alias block)))
  684. alias-properties (when (seq alias)
  685. [[:block/alias alias]])
  686. remove-built-in-properties (fn [properties]
  687. (remove (fn [[id _]]
  688. (when-let [ent (db/entity id)]
  689. (and (not (get-in ent [:block/schema :public?]))
  690. (ldb/built-in? ent))))
  691. properties))
  692. {:keys [classes all-classes classes-properties]} (db-property-handler/get-block-classes-properties (:db/id block))
  693. one-class? (= 1 (count classes))
  694. block-own-properties (->> (concat (seq alias-properties)
  695. (seq properties))
  696. remove-built-in-properties
  697. (remove (fn [[id _]] ((set classes-properties) id))))
  698. root-block? (= (:id opts) (str (:block/uuid block)))
  699. ;; This section produces own-properties and full-hidden-properties
  700. hide-with-property-id (fn [property-id]
  701. (if (or root-block? page? page-configure?)
  702. false
  703. (boolean (:hide? (:block/schema (db/entity property-id))))))
  704. property-hide-f (cond
  705. config/publishing?
  706. ;; Publishing is read only so hide all blank properties as they
  707. ;; won't be edited and distract from properties that have values
  708. (fn [[property-id property-value]]
  709. (or (nil? property-value)
  710. (hide-with-property-id property-id)))
  711. (:ui/hide-empty-properties? (state/get-config))
  712. (fn [[property-id property-value]]
  713. ;; User's selection takes precedence over config
  714. (if (contains? (:block/schema (db/entity property-id)) :hide?)
  715. (hide-with-property-id property-id)
  716. (nil? property-value)))
  717. :else
  718. (comp hide-with-property-id first))
  719. {_block-hidden-properties true
  720. block-own-properties' false} (group-by property-hide-f block-own-properties)
  721. {_class-hidden-properties true
  722. class-own-properties false} (group-by property-hide-f
  723. (map (fn [id] [id (get block-properties id)]) classes-properties))
  724. own-properties (->>
  725. (if one-class?
  726. (concat block-own-properties' class-own-properties)
  727. block-own-properties'))
  728. class->properties (loop [classes all-classes
  729. properties #{}
  730. result []]
  731. (if-let [class (first classes)]
  732. (let [cur-properties (->> (db-property/get-class-ordered-properties class)
  733. (remove properties)
  734. (remove hide-with-property-id))]
  735. (recur (rest classes)
  736. (set/union properties (set cur-properties))
  737. (if (seq cur-properties)
  738. (conj result [class cur-properties])
  739. result)))
  740. result))
  741. keyboard-triggered? (= (state/sub :editor/new-property-input-id) edit-input-id)]
  742. (when-not (and (empty? block-own-properties')
  743. (empty? class->properties)
  744. (not (:page-configure? opts))
  745. (not keyboard-triggered?))
  746. [:div.ls-properties-area
  747. (cond-> (if in-block-container?
  748. {:id id}
  749. {:id id
  750. :class (when class-schema? "class-properties")})
  751. (:selected? opts)
  752. (update :class conj "select-none")
  753. true (assoc :tab-index 0
  754. :on-key-up #(when-let [block (and (= "Escape" (.-key %))
  755. (.closest (.-target %) "[blockid]"))]
  756. (state/set-selection-blocks! [block])
  757. (some-> js/document.activeElement (.blur)))))
  758. (properties-section block (if class-schema? properties own-properties) opts)
  759. (rum/with-key (new-property block id keyboard-triggered? opts) (str id "-add-property"))
  760. (when (and (seq class->properties) (not one-class?))
  761. (let [page-cp (:page-cp opts)]
  762. [:div.parent-properties.flex.flex-1.flex-col.gap-1
  763. (for [[class class-properties] class->properties]
  764. (let [id-properties (->> class-properties
  765. remove-built-in-properties
  766. (map (fn [id] [id (get block-properties id)])))]
  767. (when (seq id-properties)
  768. [:div
  769. (when page-cp
  770. [:span.text-sm (page-cp {} class)])
  771. (properties-section block id-properties opts)])))]))])))