config.cljs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. (ns frontend.components.property.config
  2. (:require [clojure.string :as string]
  3. [frontend.components.dnd :as dnd]
  4. [frontend.components.icon :as icon-component]
  5. [frontend.config :as config]
  6. [frontend.handler.common.developer :as dev-common-handler]
  7. [frontend.handler.db-based.property :as db-property-handler]
  8. [frontend.db :as db]
  9. [frontend.db.async :as db-async]
  10. [frontend.handler.property :as property-handler]
  11. [frontend.handler.route :as route-handler]
  12. [frontend.state :as state]
  13. [frontend.util :as util]
  14. [logseq.db :as ldb]
  15. [logseq.db.frontend.order :as db-order]
  16. [logseq.db.frontend.property :as db-property]
  17. [logseq.db.frontend.property.type :as db-property-type]
  18. [logseq.outliner.core :as outliner-core]
  19. [logseq.shui.ui :as shui]
  20. [logseq.shui.popup.core :as shui-popup]
  21. [promesa.core :as p]
  22. [goog.dom :as gdom]
  23. [rum.core :as rum]
  24. [frontend.db-mixins :as db-mixins]
  25. [frontend.components.property.value :as pv]
  26. [frontend.components.select :as select]
  27. [frontend.db.model :as model]
  28. [frontend.handler.db-based.page :as db-page-handler]
  29. [frontend.ui :as ui]))
  30. (defn- re-init-commands!
  31. "Update commands after task status and priority's closed values has been changed"
  32. [property]
  33. (when (contains? #{:logseq.task/status :logseq.task/priority} (:db/ident property))
  34. (state/pub-event! [:init/commands])))
  35. (defn- <upsert-closed-value!
  36. "Create new closed value and returns its block UUID."
  37. [property item]
  38. (p/do!
  39. (db-property-handler/upsert-closed-value! (:db/ident property) item)
  40. (re-init-commands! property)))
  41. (defn- loop-focusable-elements!
  42. ([^js cnt] (loop-focusable-elements! cnt
  43. ".ui__button:not([disabled]), .ui__input, .ui__textarea"))
  44. ([^js cnt selectors]
  45. (when-let [els (some-> cnt (.querySelectorAll selectors) (seq))]
  46. (let [active js/document.activeElement
  47. current-idx (.indexOf els active)
  48. total-len (count els)
  49. to-idx (cond
  50. (or (= -1 current-idx)
  51. (= total-len (inc current-idx)))
  52. 0
  53. :else
  54. (inc current-idx))]
  55. (some-> els (nth to-idx) (.focus))))))
  56. (defn- set-property-description!
  57. [property description]
  58. (if-let [ent (:logseq.property/description property)]
  59. (db/transact! (state/get-current-repo)
  60. [(outliner-core/block-with-updated-at
  61. {:db/id (:db/id ent) :block/title description})]
  62. {:outliner-op :save-block})
  63. (when-not (string/blank? description)
  64. (db-property-handler/set-block-property!
  65. (:db/id property)
  66. :logseq.property/description description))))
  67. (defn- <create-class-if-not-exists!
  68. [value]
  69. (when (string? value)
  70. (let [page-name (string/trim value)]
  71. (when-not (string/blank? page-name)
  72. (p/let [page (db-page-handler/<create-class! page-name {:redirect? false
  73. :create-first-block? false})]
  74. (:block/uuid page))))))
  75. (rum/defc class-select
  76. [property {:keys [multiple-choices? disabled? default-open? no-class? on-hide]
  77. :or {multiple-choices? true}}]
  78. (let [*ref (rum/use-ref nil)]
  79. (rum/use-effect!
  80. (fn []
  81. (when default-open?
  82. (some-> (rum/deref *ref)
  83. (.click))))
  84. [default-open?])
  85. (let [schema-classes (:property/schema.classes property)]
  86. [:div.flex.flex-1.col-span-3
  87. (let [content-fn
  88. (fn [{:keys [id]}]
  89. (let [toggle-fn #(do
  90. (when (fn? on-hide) (on-hide))
  91. (shui/popup-hide! id))
  92. classes (model/get-all-classes (state/get-current-repo) {:except-root-class? true})
  93. options (map (fn [class]
  94. {:label (:block/title class)
  95. :value (:block/uuid class)})
  96. classes)
  97. options (if no-class?
  98. (cons {:label "Skip choosing tag"
  99. :value :no-tag}
  100. options)
  101. options)
  102. opts {:items options
  103. :input-default-placeholder (if multiple-choices? "Choose tags" "Choose tag")
  104. :dropdown? false
  105. :close-modal? false
  106. :multiple-choices? multiple-choices?
  107. :selected-choices (map :block/uuid schema-classes)
  108. :extract-fn :label
  109. :extract-chosen-fn :value
  110. :show-new-when-not-exact-match? true
  111. :input-opts {:on-key-down
  112. (fn [e]
  113. (case (util/ekey e)
  114. "Escape"
  115. (do
  116. (util/stop e)
  117. (toggle-fn))
  118. nil))}
  119. :on-chosen (fn [value select?]
  120. (if (= value :no-tag)
  121. (toggle-fn)
  122. (p/let [result (<create-class-if-not-exists! value)
  123. value' (or result value)
  124. tx-data [[(if select? :db/add :db/retract) (:db/id property) :property/schema.classes [:block/uuid value']]]
  125. _ (db/transact! (state/get-current-repo) tx-data {:outliner-op :update-property})]
  126. (when-not multiple-choices? (toggle-fn)))))}]
  127. (select/select opts)))]
  128. [:div.flex.flex-1.cursor-pointer
  129. {:ref *ref
  130. :on-click (if disabled?
  131. (constantly nil)
  132. #(shui/popup-show! (.-target %) content-fn {:id :ls-node-tags-sub-pane}))}
  133. (if (seq schema-classes)
  134. [:div.flex.flex-1.flex-row.items-center.flex-wrap.gap-2
  135. {:class "max-w-[300px]"}
  136. (for [class schema-classes]
  137. [:a.text-sm (str "#" (:block/title class))])
  138. [:span.opacity-60.pl-1.top-1.relative.hover:opacity-80.active:opacity-60
  139. (shui/tabler-icon "edit")]]
  140. (pv/property-empty-btn-value property))])])))
  141. (rum/defc name-edit-pane
  142. [property {:keys [set-sub-open! disabled?]}]
  143. (let [*form-data (rum/use-ref {:icon (:logseq.property/icon property)
  144. :title (or (:block/title property) "")
  145. :description (or (db-property/property-value-content (:logseq.property/description property)) "")})
  146. [form-data, set-form-data!] (rum/use-state (rum/deref *form-data))
  147. [saving?, set-saving!] (rum/use-state false)
  148. *el (rum/use-ref nil)
  149. *input-ref (rum/use-ref nil)
  150. title (util/trim-safe (:title form-data))
  151. description (util/trim-safe (:description form-data))]
  152. (rum/use-effect!
  153. (fn []
  154. (js/setTimeout #(some-> (rum/deref *el) (.focus)) 32))
  155. [])
  156. [:div.ls-property-name-edit-pane.outline-none
  157. {:on-key-down (fn [^js e] (when (= "Tab" (.-key e))
  158. (loop-focusable-elements! (rum/deref *el))))
  159. :tab-index -1
  160. :ref *el}
  161. [:div.flex.items-center.input-wrap
  162. (icon-component/icon-picker (:icon form-data)
  163. {:on-chosen (fn [_e icon] (set-form-data! (assoc form-data :icon icon)))
  164. :popup-opts {:align "start"}
  165. :del-btn? (boolean (:icon form-data))
  166. :empty-label "?"})
  167. (shui/input {:ref *input-ref :size "sm" :default-value title :placeholder "name"
  168. :disabled disabled? :on-change (fn [^js e] (set-form-data! (assoc form-data :title (util/trim-safe (util/evalue e)))))})]
  169. [:div.pt-2 (shui/textarea {:placeholder "description" :default-value description
  170. :disabled disabled? :on-change (fn [^js e] (set-form-data! (assoc form-data :description (util/trim-safe (util/evalue e)))))})]
  171. (let [dirty? (not= (rum/deref *form-data) form-data)]
  172. [:div.pt-2.flex.justify-end
  173. (shui/button {:size "sm" :disabled (or saving? (not dirty?))
  174. :variant (if dirty? :default :secondary)
  175. :on-click (fn []
  176. (set-saving! true)
  177. (-> [(db-property-handler/upsert-property!
  178. (:db/ident property)
  179. (:block/schema property)
  180. {:property-name title
  181. :properties {:logseq.property/icon (:icon form-data)}})
  182. (when (not= description (:description (rum/deref *form-data)))
  183. (set-property-description! property description))]
  184. (p/all)
  185. (p/then #(set-sub-open! false))
  186. (p/catch #(shui/toast! (str %) :error))
  187. (p/finally #(set-saving! false))))}
  188. "Save")])]))
  189. (rum/defc choice-base-edit-form
  190. [own-property block]
  191. (let [create? (:create? block)
  192. uuid (:block/uuid block)
  193. *form-data (rum/use-ref
  194. {:value (or (str (db-property/closed-value-content block)) "")
  195. :icon (:logseq.property/icon block)
  196. :description (or (db-property/property-value-content (:logseq.property/description block)) "")})
  197. [form-data, set-form-data!] (rum/use-state (rum/deref *form-data))
  198. *input-ref (rum/use-ref nil)]
  199. (rum/use-effect!
  200. (fn []
  201. (when create?
  202. (js/setTimeout #(some-> (rum/deref *input-ref) (.focus)) 60)))
  203. [])
  204. [:div.ls-base-edit-form
  205. [:div.flex.items-center.input-wrap
  206. (icon-component/icon-picker
  207. (:icon form-data)
  208. {:on-chosen (fn [_e icon] (set-form-data! (assoc form-data :icon icon)))
  209. :empty-label "?"
  210. :del-btn? (boolean (:icon form-data))
  211. :popup-opts {:align "start"}})
  212. (shui/input {:ref *input-ref :size "sm"
  213. :default-value (:value form-data)
  214. :on-change (fn [^js e] (set-form-data! (assoc form-data :value (util/trim-safe (util/evalue e)))))
  215. :placeholder "title"})]
  216. [:div.pt-2 (shui/textarea
  217. {:placeholder "description" :default-value (:description form-data)
  218. :on-change (fn [^js e] (set-form-data! (assoc form-data :description (util/trim-safe (util/evalue e)))))})]
  219. [:div.pt-2.flex.justify-end
  220. (let [dirty? (not= (rum/deref *form-data) form-data)]
  221. (shui/button {:size "sm"
  222. :disabled (not dirty?)
  223. :on-click (fn []
  224. (-> (<upsert-closed-value! own-property
  225. (cond-> form-data uuid (assoc :id uuid)))
  226. (p/then #(shui/popup-hide!))
  227. (p/catch #(shui/toast! (str %) :error))))
  228. :variant (if dirty? :default :secondary)}
  229. "Save"))]]))
  230. (defn restore-root-highlight-item!
  231. [id]
  232. (js/setTimeout
  233. #(some-> (gdom/getElement id) (.focus)) 32))
  234. (rum/defc dropdown-editor-menuitem
  235. [{:keys [id icon title desc submenu-content item-props sub-content-props disabled? toggle-checked? on-toggle-checked-change]}]
  236. (let [submenu-content (when-not disabled? submenu-content)
  237. item-props' (if (and disabled? (:on-select item-props))
  238. (assoc item-props :on-select (fn [] nil))
  239. item-props)
  240. [sub-open? set-sub-open!] (rum/use-state false)
  241. toggle? (boolean? toggle-checked?)
  242. id1 (str (or id icon (random-uuid)))
  243. id2 (str "d2-" id1)
  244. or-close-menu-sub! (fn []
  245. (when (and (not (shui-popup/get-popup :ls-icon-picker))
  246. (not (shui-popup/get-popup :ls-base-edit-form))
  247. (not (shui-popup/get-popup :ls-node-tags-sub-pane)))
  248. (set-sub-open! false)
  249. (restore-root-highlight-item! id1)))
  250. wrap-menuitem (if submenu-content
  251. #(shui/dropdown-menu-sub
  252. {:open sub-open?
  253. :on-open-change (fn [v] (if v (set-sub-open! true) (or-close-menu-sub!)))}
  254. (shui/dropdown-menu-sub-trigger (merge {:id id1} item-props') %)
  255. (shui/dropdown-menu-portal
  256. (shui/dropdown-menu-sub-content
  257. (merge {:hideWhenDetached true
  258. :onEscapeKeyDown or-close-menu-sub!} sub-content-props)
  259. (if (fn? submenu-content)
  260. (submenu-content {:set-sub-open! set-sub-open! :id id1}) submenu-content))))
  261. #(shui/dropdown-menu-item
  262. (merge {:on-select (fn []
  263. (when toggle?
  264. (some-> (gdom/getElement id2) (.click))))
  265. :id id1}
  266. item-props') %))]
  267. (wrap-menuitem
  268. [:div.inner-wrap
  269. {:class (util/classnames [{:disabled disabled?}])}
  270. [:strong
  271. (some-> icon (name) (shui/tabler-icon {:size 14
  272. :style {:margin-top "-1"}}))
  273. [:span title]]
  274. (if (fn? desc) (desc)
  275. (if (boolean? toggle-checked?)
  276. [:span.scale-90.flex.items-center
  277. (shui/switch {:id id2 :size "sm" :checked toggle-checked?
  278. :disabled disabled? :on-click #(util/stop-propagation %)
  279. :on-checked-change (or on-toggle-checked-change identity)})]
  280. [:label [:span desc]
  281. (when disabled? (shui/tabler-icon "forbid-2" {:size 15}))]))])))
  282. (rum/defc choice-item-content
  283. [property block]
  284. (let [delete-choice! (fn []
  285. (p/do!
  286. (db-property-handler/delete-closed-value! (:db/id property) (:db/id block))
  287. (re-init-commands! property)))
  288. update-icon! (fn [icon]
  289. (property-handler/set-block-property!
  290. (state/get-current-repo) (:block/uuid block) :logseq.property/icon
  291. (select-keys icon [:id :type :color])))
  292. icon (:logseq.property/icon block)
  293. value (db-property/closed-value-content block)]
  294. [:li
  295. (shui/tabler-icon "grip-vertical" {:size 14})
  296. (shui/button {:size "sm" :variant :outline}
  297. (icon-component/icon-picker icon {:on-chosen (fn [_e icon] (update-icon! icon))
  298. :popup-opts {:align "start"}
  299. :del-btn? (boolean icon)
  300. :empty-label "?"}))
  301. [:strong {:on-click (fn [^js e]
  302. (shui/popup-show! (.-target e)
  303. (fn [] (choice-base-edit-form property block))
  304. {:id :ls-base-edit-form
  305. :align "start"}))}
  306. value]
  307. [:a.del {:on-click delete-choice!
  308. :title "Delete this choice"}
  309. (shui/tabler-icon "x" {:size 16})]]))
  310. (rum/defc add-existing-values
  311. [property values {:keys [toggle-fn]}]
  312. (let [uuid-values? (every? uuid? values)
  313. values' (if uuid-values?
  314. (let [values' (map #(db/entity [:block/uuid %]) values)]
  315. (->> (util/distinct-by db-property/closed-value-content values')
  316. (map :block/uuid)))
  317. values)]
  318. [:div.flex.flex-col.gap-1.w-64.p-4.overflow-y-auto
  319. {:class "max-h-[50dvh]"}
  320. [:div "Existing values:"]
  321. [:ol
  322. (for [value values']
  323. [:li (if (uuid? value)
  324. (let [result (db/entity [:block/uuid value])]
  325. (db-property/closed-value-content result))
  326. (str value))])]
  327. (shui/button
  328. {:on-click (fn []
  329. (p/let [_ (db-property-handler/add-existing-values-to-closed-values! (:db/id property) values')]
  330. (toggle-fn)))}
  331. "Add choices")]))
  332. (rum/defc choices-sub-pane < rum/reactive db-mixins/query
  333. [property]
  334. (let [values (:property/closed-values property)
  335. choices (doall
  336. (keep (fn [value]
  337. (when-let [block (db/sub-block (:db/id value))]
  338. (let [id (:block/uuid block)]
  339. {:id (str id)
  340. :value id
  341. :content (choice-item-content property block)})))
  342. values))]
  343. [:div.ls-property-dropdown-editor.ls-property-choices-sub-pane
  344. (when (seq choices)
  345. [:ul.choices-list
  346. (dnd/items choices
  347. {:sort-by-inner-element? false
  348. :on-drag-end (fn [_ {:keys [active-id over-id direction]}]
  349. (let [move-down? (= direction :down)
  350. over (db/entity [:block/uuid (uuid over-id)])
  351. active (db/entity [:block/uuid (uuid active-id)])
  352. over-order (:block/order over)
  353. new-order (if move-down?
  354. (let [next-order (db-order/get-next-order (db/get-db) property (:db/id over))]
  355. (db-order/gen-key over-order next-order))
  356. (let [prev-order (db-order/get-prev-order (db/get-db) property (:db/id over))]
  357. (db-order/gen-key prev-order over-order)))]
  358. (db/transact! (state/get-current-repo)
  359. [{:db/id (:db/id active)
  360. :block/order new-order}
  361. (outliner-core/block-with-updated-at
  362. {:db/id (:db/id property)})]
  363. {:outliner-op :save-block})))})])
  364. ;; add choice
  365. (dropdown-editor-menuitem
  366. {:icon :plus :title "Add choice"
  367. :item-props {:on-click
  368. (fn [^js e]
  369. (p/let [values (db-async/<get-block-property-values (state/get-current-repo) (:db/ident property))
  370. existing-values (seq (:property/closed-values property))
  371. values (if (seq existing-values)
  372. (let [existing-ids (set (map :db/id existing-values))]
  373. (remove (fn [id] (existing-ids id)) values))
  374. values)]
  375. (shui/popup-show! (.-target e)
  376. (fn [{:keys [id]}]
  377. (let [opts {:toggle-fn (fn [] (shui/popup-hide! id))}
  378. values' (->> (if (contains? db-property-type/user-ref-property-types (get-in property [:block/schema :type]))
  379. (map #(:block/uuid (db/entity %)) values)
  380. values)
  381. (remove string/blank?)
  382. distinct)]
  383. (if (seq values')
  384. (add-existing-values property values' opts)
  385. (choice-base-edit-form property {:create? true}))))
  386. {:id :ls-base-edit-form
  387. :align "start"})))}})]))
  388. (def position-labels
  389. {:properties {:icon :layout-distribute-horizontal :title "Block properties"}
  390. :block-left {:icon :layout-align-right :title "Beginning of the block"}
  391. :block-right {:icon :layout-align-left :title "End of the block"}
  392. :block-below {:icon :layout-align-top :title "Below of the block"}})
  393. (rum/defc ui-position-sub-pane
  394. [property {:keys [id set-sub-open! _position]}]
  395. (let [handle-select! (fn [^js e]
  396. (when-let [v (some-> (.-target e) (.-dataset) (.-value))]
  397. (db-property-handler/upsert-property!
  398. (:db/ident property)
  399. (assoc (:block/schema property) :position (keyword v))
  400. {:property-name (:block/title property)})
  401. (set-sub-open! false)
  402. (restore-root-highlight-item! id)))
  403. item-props {:on-select handle-select!}]
  404. [:div.ls-property-dropdown-editor.ls-property-ui-position-sub-pane
  405. (for [[k v] position-labels]
  406. (let [item-props (assoc item-props :data-value k)]
  407. (dropdown-editor-menuitem
  408. (assoc v :item-props item-props))))]))
  409. (defn property-type-label
  410. [property-type]
  411. (case property-type
  412. :default
  413. "Text"
  414. :datetime
  415. "DateTime"
  416. ((comp string/capitalize name) property-type)))
  417. (defn- handle-delete-property!
  418. [block property & {:keys [class? class-schema?]}]
  419. (let [class? (or class? (ldb/class? block))
  420. remove! #(let [repo (state/get-current-repo)]
  421. (if (and class? class-schema?)
  422. (db-property-handler/class-remove-property! (:db/id block) (:db/id property))
  423. (property-handler/remove-block-property! repo (:block/uuid block) (:db/ident property))))]
  424. (if (and class? class-schema?)
  425. (-> (shui/dialog-confirm!
  426. [:p (str "Are you sure you want to delete the property from this tag?")]
  427. {:id :delete-property-from-class
  428. :data-reminder :ok})
  429. (p/then remove!))
  430. (-> (shui/dialog-confirm!
  431. "Are you sure you want to delete the property from this node?"
  432. {:id :delete-property-from-node
  433. :data-reminder :ok})
  434. (p/then remove!)))))
  435. (rum/defc property-type-sub-pane
  436. [property {:keys [id set-sub-open! _position]}]
  437. (let [handle-select! (fn [^js e]
  438. (when-let [v (some-> (.-target e) (.-dataset) (.-value))]
  439. (p/do!
  440. (db-property-handler/upsert-property!
  441. (:db/ident property)
  442. (assoc (:block/schema property) :type (keyword v))
  443. {})
  444. (set-sub-open! false)
  445. (restore-root-highlight-item! id))))
  446. item-props {:on-select handle-select!}
  447. schema-types (->> db-property-type/user-built-in-property-types
  448. (map (fn [type]
  449. {:label (property-type-label type)
  450. :value type})))]
  451. [:div.ls-property-dropdown-editor.ls-property-type-sub-pane
  452. (for [{:keys [label value]} schema-types]
  453. (let [option {:id label
  454. :title label
  455. :desc ""
  456. :item-props (assoc item-props :data-value value)}]
  457. (dropdown-editor-menuitem option)))]))
  458. (rum/defc ^:large-vars/cleanup-todo dropdown-editor-impl
  459. "property: block entity"
  460. [property owner-block values {:keys [class-schema? debug?]}]
  461. (let [title (:block/title property)
  462. property-schema (:block/schema property)
  463. property-type (get property-schema :type)
  464. property-type-label' (some-> property-type (property-type-label))
  465. enable-closed-values? (contains? db-property-type/closed-value-property-types
  466. (or property-type :default))
  467. icon (:logseq.property/icon property)
  468. icon (when icon [:span.float-left.w-4.h-4.overflow-hidden.leading-4.relative
  469. (icon-component/icon icon {:size 15})])
  470. built-in? (ldb/built-in? property)
  471. disabled? (or built-in? config/publishing?)]
  472. [:<>
  473. (dropdown-editor-menuitem {:icon :pencil :title "Property name" :desc [:span.flex.items-center.gap-1 icon title]
  474. :submenu-content (fn [ops] (name-edit-pane property (assoc ops :disabled? disabled?)))})
  475. (let [disabled?' (or disabled? (and property-type (seq values)))]
  476. (dropdown-editor-menuitem {:icon :letter-t
  477. :title "Property type"
  478. :desc (if disabled?'
  479. (ui/tippy {:html [:div.w-96
  480. "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."]
  481. :class "tippy-hover ml-2"
  482. :interactive true
  483. :disabled false}
  484. (str property-type-label'))
  485. (str property-type-label'))
  486. :disabled? disabled?'
  487. :submenu-content (fn [ops]
  488. (property-type-sub-pane property ops))}))
  489. (when (and (= property-type :node)
  490. (not (contains? #{:logseq.property/parent} (:db/ident property))))
  491. (dropdown-editor-menuitem {:icon :hash
  492. :title "Specify node tags"
  493. :desc ""
  494. :submenu-content (fn [_ops]
  495. [:div.px-4
  496. (class-select property {:default-open? false})])}))
  497. (when enable-closed-values?
  498. (let [values (:property/closed-values property)]
  499. (dropdown-editor-menuitem {:icon :list :title "Available choices"
  500. :desc (when (seq values) (str (count values) " choices"))
  501. :submenu-content (fn [] (choices-sub-pane property))})))
  502. (let [many? (db-property/many? property)]
  503. (dropdown-editor-menuitem {:icon :checks :title "Multiple values"
  504. :toggle-checked? many?
  505. :disabled? (or disabled? (not (contains? db-property-type/cardinality-property-types property-type)))
  506. :on-toggle-checked-change
  507. (fn []
  508. (let [update-cardinality-fn #(db-property-handler/upsert-property! (:db/ident property)
  509. (assoc property-schema :cardinality (if many? :one :many)) {})]
  510. ;; Only show dialog for existing values as it can be reversed for unused properties
  511. (if (and (seq values) (not many?))
  512. (-> (shui/dialog-confirm!
  513. "This action cannot be undone. Do you want to change this property to have multiple values?")
  514. (p/then update-cardinality-fn))
  515. (update-cardinality-fn))))}))
  516. (let [group' (->> [(when (and (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
  517. (not
  518. (and (= :default (get-in property [:block/schema :type]))
  519. (empty? (:property/closed-values property))
  520. (contains? #{nil :properties} (:position property-schema)))))
  521. (let [position (:position property-schema)]
  522. (dropdown-editor-menuitem {:icon :float-left :title "UI position" :desc (some->> position (get position-labels) (:title))
  523. :item-props {:class "ui__position-trigger-item"}
  524. :submenu-content (fn [ops] (ui-position-sub-pane property (assoc ops :position position)))})))
  525. (when (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
  526. (dropdown-editor-menuitem {:icon :eye-off :title "Hide by default" :toggle-checked? (boolean (:hide? property-schema))
  527. :on-toggle-checked-change #(db-property-handler/upsert-property! (:db/ident property)
  528. (assoc property-schema :hide? %) {})}))]
  529. (remove nil?))]
  530. (when (> (count group') 0)
  531. (cons (shui/dropdown-menu-separator) group')))
  532. (when owner-block
  533. [:<>
  534. (shui/dropdown-menu-separator)
  535. (dropdown-editor-menuitem
  536. {:icon :share-3 :title "Go to this property" :desc ""
  537. :item-props {:class "opacity-90 focus:opacity-100"
  538. :on-select (fn []
  539. (shui/popup-hide-all!)
  540. (route-handler/redirect-to-page! (:block/uuid property)))}})])
  541. (when (and owner-block
  542. (not (and
  543. (ldb/class? owner-block)
  544. (contains? #{:logseq.property/parent} (:db/ident property)))))
  545. (dropdown-editor-menuitem
  546. {:id :delete-property :icon :x
  547. :title (if class-schema? "Delete property from tag" "Delete property from node")
  548. :desc "" :disabled? false
  549. :item-props {:class "opacity-60 focus:!text-red-rx-09 focus:opacity-100"
  550. :on-select (fn [^js e]
  551. (util/stop e)
  552. (-> (p/do!
  553. (handle-delete-property! owner-block property {:class-schema? class-schema?})
  554. (shui/popup-hide-all!))
  555. (p/catch (fn [] (restore-root-highlight-item! :delete-property)))))}}))
  556. (when debug?
  557. [:<>
  558. (shui/dropdown-menu-separator)
  559. (dropdown-editor-menuitem
  560. {:icon :bug :title (str (:db/ident property)) :desc "" :disabled? false
  561. :item-props {:class "opacity-60 focus:opacity-100 focus:!text-red-rx-08"
  562. :on-select (fn []
  563. (dev-common-handler/show-entity-data (:db/id property))
  564. (shui/popup-hide!))}})])]))
  565. (rum/defcs dropdown-editor < rum/reactive db-mixins/query
  566. {:init (fn [state]
  567. (let [*values (atom :loading)
  568. repo (state/get-current-repo)
  569. property (first (:rum/args state))
  570. ident (:db/ident property)]
  571. (p/let [_ (db-async/<get-block repo (:block/uuid property))
  572. result (db-async/<get-block-property-values repo ident)]
  573. (reset! *values result))
  574. (assoc state ::values *values)))}
  575. [state property* owner-block opts]
  576. (let [property (db/sub-block (:db/id property*))
  577. values (rum/react (::values state))]
  578. (when-not (= :loading values)
  579. (dropdown-editor-impl property owner-block values opts))))