right_sidebar.cljs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. (ns frontend.components.right-sidebar
  2. (:require [cljs-bean.core :as bean]
  3. [clojure.string :as string]
  4. [frontend.components.block :as block]
  5. [frontend.components.cmdk.core :as cmdk]
  6. [frontend.components.icon :as icon]
  7. [frontend.components.onboarding :as onboarding]
  8. [frontend.components.page :as page]
  9. [frontend.components.profiler :as profiler]
  10. [frontend.components.shortcut-help :as shortcut-help]
  11. [frontend.components.vector-search.sidebar :as vector-search]
  12. [frontend.config :as config]
  13. [frontend.context.i18n :refer [t]]
  14. [frontend.date :as date]
  15. [frontend.db :as db]
  16. [frontend.db.async :as db-async]
  17. [frontend.db.rtc.debug-ui :as rtc-debug-ui]
  18. [frontend.handler.editor :as editor-handler]
  19. [frontend.handler.route :as route-handler]
  20. [frontend.handler.ui :as ui-handler]
  21. [frontend.state :as state]
  22. [frontend.ui :as ui]
  23. [frontend.util :as util]
  24. [logseq.db :as ldb]
  25. [logseq.shui.hooks :as hooks]
  26. [logseq.shui.ui :as shui]
  27. [medley.core :as medley]
  28. [promesa.core :as p]
  29. [rum.core :as rum]))
  30. (rum/defc toggle
  31. []
  32. (when-not (util/sm-breakpoint?)
  33. (ui/with-shortcut :ui/toggle-right-sidebar "left"
  34. (shui/button-ghost-icon :layout-sidebar-right
  35. {:title (t :right-side-bar/toggle-right-sidebar)
  36. :class "toggle-right-sidebar"
  37. :on-click ui-handler/toggle-right-sidebar!}))))
  38. (rum/defc block-cp < rum/reactive
  39. [repo idx block]
  40. (let [id (:block/uuid block)]
  41. [:div.mt-2
  42. (page/page-cp {:parameters {:path {:name (str id)}}
  43. :sidebar? true
  44. :sidebar/idx idx
  45. :repo repo})]))
  46. (defn get-scrollable-container
  47. []
  48. (js/document.querySelector ".sidebar-item-list"))
  49. (rum/defc page-cp < rum/reactive
  50. [repo page-name]
  51. (page/page-cp {:parameters {:path {:name page-name}}
  52. :sidebar? true
  53. :scroll-container (get-scrollable-container)
  54. :repo repo}))
  55. (rum/defc shortcut-settings
  56. []
  57. [:div.contents.flex-col.flex.ml-3
  58. (shortcut-help/shortcut-page {:show-title? false})])
  59. (defn- block-with-breadcrumb
  60. [repo block idx sidebar-key ref?]
  61. (when-let [block-id (:block/uuid block)]
  62. [[:.flex.items-center {:class (when ref? "ml-2")}
  63. (block/breadcrumb {:id "block-parent"
  64. :block? true
  65. :sidebar-key sidebar-key} repo block-id {:indent? false})]
  66. (block-cp repo idx block)]))
  67. (rum/defc search-title < rum/reactive
  68. [*input]
  69. (let [input (rum/react *input)
  70. input' (if (string/blank? input) "Blank input" input)]
  71. [:span.overflow-hidden.text-ellipsis input']))
  72. (rum/defc sidebar-search
  73. [repo block-type init-key input *input]
  74. (rum/with-key
  75. (cmdk/cmdk-block {:initial-input input
  76. :sidebar? true
  77. :on-input-change (fn [new-value]
  78. (reset! *input new-value))
  79. :on-input-blur (fn [new-value]
  80. (state/sidebar-replace-block! [repo input block-type]
  81. [repo new-value block-type]))})
  82. (str init-key)))
  83. (defn- <build-sidebar-item
  84. [repo idx db-id block-type *db-id init-key]
  85. (->
  86. (p/do!
  87. (when-not (contains? #{:contents :search} block-type)
  88. (db-async/<get-block repo db-id))
  89. (let [lookup (cond
  90. (integer? db-id) db-id
  91. (uuid? db-id) [:block/uuid db-id]
  92. :else nil)
  93. entity (when lookup (db/entity repo lookup))
  94. page? (ldb/page? entity)
  95. block-render (fn []
  96. (when entity
  97. (if page?
  98. [[:.flex.items-center.page-title.gap-1
  99. (icon/get-node-icon-cp entity {:class "text-md"})
  100. [:span.overflow-hidden.text-ellipsis (:block/title entity)]]
  101. (page-cp repo (str (:block/uuid entity)))]
  102. (block-with-breadcrumb repo entity idx [repo db-id block-type] false))))]
  103. (case (keyword block-type)
  104. :contents
  105. (when-let [page (db/get-page "Contents")]
  106. [[:.flex.items-center (ui/icon "list-details" {:class "text-md mr-2"}) (t :right-side-bar/contents)]
  107. (page-cp repo (str (:block/uuid page)))])
  108. :help
  109. [[:.flex.items-center (ui/icon "help" {:class "text-md mr-2"}) (t :right-side-bar/help)] (onboarding/help)]
  110. :page-graph
  111. [[:.flex.items-center (ui/icon "hierarchy" {:class "text-md mr-2"}) (t :right-side-bar/page-graph)]
  112. (page/page-graph)]
  113. :block-ref
  114. (let [lookup (if (integer? db-id) db-id [:block/uuid db-id])]
  115. (when-let [block (db/entity repo lookup)]
  116. [(t :right-side-bar/block-ref)
  117. (block-with-breadcrumb repo block idx [repo db-id block-type] true)]))
  118. :block
  119. (block-render)
  120. :page
  121. (block-render)
  122. :search
  123. [[:.flex.items-center.page-title
  124. (ui/icon "search" {:class "text-md mr-2"})
  125. (search-title *db-id)]
  126. (sidebar-search repo block-type init-key db-id *db-id)]
  127. :shortcut-settings
  128. [[:.flex.items-center (ui/icon "command" {:class "text-md mr-2"}) (t :help/shortcuts)]
  129. (shortcut-settings)]
  130. :rtc
  131. [[:.flex.items-center (ui/icon "cloud" {:class "text-md mr-2"}) "(Dev) RTC"]
  132. (rtc-debug-ui/rtc-debug-ui)]
  133. :profiler
  134. [[:.flex.items-center (ui/icon "cloud" {:class "text-md mr-2"}) "(Dev) Profiler"]
  135. (profiler/profiler)]
  136. :vector-search
  137. [[:.flex.items-center (ui/icon "file-search" {:class "text-md mr-2"}) "(Dev) VectorSearch"]
  138. (vector-search/vector-search-sidebar)]
  139. ["" [:span]])))
  140. (p/catch (fn [error]
  141. (js/console.error error)))))
  142. (defonce *drag-to
  143. (atom nil))
  144. (defonce *drag-from
  145. (atom nil))
  146. (rum/defc actions-menu-content
  147. [db-id idx type collapsed? block-count]
  148. (let [multi-items? (> block-count 1)
  149. menu-item shui/dropdown-menu-item
  150. block (when (integer? db-id) (db/entity db-id))
  151. page? (or (contains? #{:page :contents} type) (ldb/page? block))]
  152. [:<>
  153. (menu-item {:on-click #(state/sidebar-remove-block! idx)} (t :right-side-bar/pane-close))
  154. (when multi-items? (menu-item {:on-click #(state/sidebar-remove-rest! db-id)} (t :right-side-bar/pane-close-others)))
  155. (when multi-items? (menu-item {:on-click (fn []
  156. (state/clear-sidebar-blocks!)
  157. (state/hide-right-sidebar!))} (t :right-side-bar/pane-close-all)))
  158. (when (and (not collapsed?) multi-items?) [:hr.menu-separator])
  159. (when-not collapsed? (menu-item {:on-click #(state/sidebar-block-toggle-collapse! db-id)} (t :right-side-bar/pane-collapse)))
  160. (when multi-items? (menu-item {:on-click #(state/sidebar-block-collapse-rest! db-id)} (t :right-side-bar/pane-collapse-others)))
  161. (when multi-items? (menu-item {:on-click #(state/sidebar-block-set-collapsed-all! true)} (t :right-side-bar/pane-collapse-all)))
  162. (when (and collapsed? multi-items?) [:hr.menu-separator])
  163. (when collapsed? (menu-item {:on-click #(state/sidebar-block-toggle-collapse! db-id)} (t :right-side-bar/pane-expand)))
  164. (when multi-items? (menu-item {:on-click #(state/sidebar-block-set-collapsed-all! false)} (t :right-side-bar/pane-expand-all)))
  165. (when page? [:hr.menu-separator])
  166. (when page?
  167. (menu-item {:on-click (fn [] (route-handler/redirect-to-page! (:block/uuid block)))}
  168. (t :right-side-bar/pane-open-as-page)))]))
  169. (rum/defc drop-indicator
  170. [idx drag-to]
  171. [:.sidebar-drop-indicator {:on-drag-enter #(when drag-to (reset! *drag-to idx))
  172. :on-drag-over util/stop
  173. :class (when (= idx drag-to) "drag-over")}])
  174. (rum/defc drop-area
  175. [idx]
  176. [:.sidebar-item-drop-area
  177. {:on-drag-over util/stop}
  178. [:.sidebar-item-drop-area-overlay.top
  179. {:on-drag-enter #(reset! *drag-to (dec idx))}]
  180. [:.sidebar-item-drop-area-overlay.bottom
  181. {:on-drag-enter #(reset! *drag-to idx)}]])
  182. (rum/defc inner-component <
  183. {:should-update (fn [_prev-state state] (last (:rum/args state)))}
  184. [component _should-update?]
  185. component)
  186. (rum/defc sidebar-item-inner
  187. [db-id {:keys [repo idx block-type collapsed? drag-from drag-to block-count *db-id init-key]}]
  188. (let [[item set-item!] (hooks/use-state nil)]
  189. (hooks/use-effect!
  190. (fn []
  191. (p/let [item (<build-sidebar-item repo idx db-id block-type *db-id init-key)]
  192. (set-item! item)))
  193. [])
  194. (when item
  195. [:<>
  196. (when (zero? idx) (drop-indicator (dec idx) drag-to))
  197. [:div.flex.sidebar-item.content.color-level.rounded-md.shadow-lg
  198. {:class [(str "item-type-" (name block-type))
  199. (when collapsed? "collapsed")]}
  200. (let [[title component] item]
  201. [:div.flex.flex-col.w-full.relative
  202. [:.flex.flex-row.justify-between.sidebar-item-header.color-level.rounded-t-md
  203. {:class (when collapsed? "rounded-b-md")
  204. :draggable true
  205. :on-context-menu (fn [e]
  206. (util/stop e)
  207. (shui/popup-show! e
  208. (actions-menu-content db-id idx block-type collapsed? block-count)
  209. {:as-dropdown? true
  210. :content-props {:on-click (fn [] (shui/popup-hide!))}}))
  211. :on-drag-start (fn [event]
  212. (editor-handler/block->data-transfer! (:block/name (db/entity db-id)) event true)
  213. (reset! *drag-from idx))
  214. :on-drag-end (fn [_event]
  215. (when drag-to (state/sidebar-move-block! idx drag-to))
  216. (reset! *drag-to nil)
  217. (reset! *drag-from nil))
  218. :on-pointer-up (fn [event]
  219. (when (= (.-which (.-nativeEvent event)) 2)
  220. (state/sidebar-remove-block! idx)))}
  221. [:button.flex.flex-row.px-2.items-center.w-full.overflow-hidden
  222. {:aria-expanded (str (not collapsed?))
  223. :id (str "sidebar-panel-header-" idx)
  224. :aria-controls (str "sidebar-panel-content-" idx)
  225. :on-click (fn [event]
  226. (util/stop event)
  227. (state/sidebar-block-toggle-collapse! db-id))}
  228. [:span.opacity-50.hover:opacity-100.flex.items-center.pr-1
  229. (ui/rotating-arrow collapsed?)]
  230. [:div.ml-1.font-medium.text-sm.overflow-hidden.whitespace-nowrap
  231. title]]
  232. [:.item-actions.flex.items-center
  233. (shui/button
  234. {:title (t :right-side-bar/pane-more)
  235. :class "px-2 py-2 h-8 w-8 text-muted-foreground"
  236. :variant :ghost
  237. :on-click #(shui/popup-show!
  238. (.-target %)
  239. (actions-menu-content db-id idx block-type collapsed? block-count)
  240. {:as-dropdown? true
  241. :content-props {:on-click (fn [] (shui/popup-hide!))}})}
  242. (ui/icon "dots"))
  243. (shui/button
  244. {:title (t :right-side-bar/pane-close)
  245. :variant :ghost
  246. :class "px-2 py-2 h-8 w-8 text-muted-foreground"
  247. :on-click #(state/sidebar-remove-block! idx)}
  248. (ui/icon "x"))]]
  249. [:div {:role "region"
  250. :id (str "sidebar-panel-content-" idx)
  251. :aria-labelledby (str "sidebar-panel-header-" idx)
  252. :class (util/classnames [{:hidden collapsed?
  253. :initial (not collapsed?)
  254. :sidebar-panel-content true
  255. :px-2 (not (contains? #{:search :shortcut-settings} block-type))}])}
  256. (inner-component component (not drag-from))]
  257. (when drag-from (drop-area idx))])]
  258. (drop-indicator idx drag-to)])))
  259. (rum/defcs sidebar-item < rum/reactive
  260. {:init (fn [state] (assoc state
  261. ::db-id (atom (nth (:rum/args state) 2))
  262. ::init-key (random-uuid)))}
  263. [state repo idx db-id block-type block-count]
  264. (let [drag-from (rum/react *drag-from)
  265. drag-to (rum/react *drag-to)]
  266. (let [collapsed? (state/sub [:ui/sidebar-collapsed-blocks db-id])]
  267. (sidebar-item-inner db-id {:repo repo
  268. :idx idx
  269. :block-type block-type
  270. :collapsed? collapsed?
  271. :drag-from drag-from
  272. :drag-to drag-to
  273. :block-count block-count
  274. :*db-id (::db-id state)
  275. :init-key (::init-key state)}))))
  276. (defn- get-page
  277. [match]
  278. (let [route-name (get-in match [:data :name])
  279. page (case route-name
  280. :page
  281. (get-in match [:path-params :name])
  282. :file
  283. (get-in match [:path-params :path])
  284. (date/journal-name))]
  285. (when page
  286. (string/lower-case page))))
  287. (defn get-current-page
  288. []
  289. (let [match (:route-match @state/state)]
  290. (get-page match)))
  291. (rum/defc sidebar-resizer
  292. [sidebar-open? sidebar-id handler-position]
  293. (let [el-ref (rum/use-ref nil)
  294. min-px-width 320 ; Custom window controls width
  295. min-ratio 0.1
  296. max-ratio 0.7
  297. keyboard-step 5
  298. add-resizing-class #(.. js/document.documentElement -classList (add "is-resizing-buf"))
  299. remove-resizing-class (fn []
  300. (.. js/document.documentElement -classList (remove "is-resizing-buf"))
  301. (reset! ui-handler/*right-sidebar-resized-at (js/Date.now)))
  302. set-width! (fn [ratio]
  303. (when el-ref
  304. (let [value (* ratio 100)
  305. width (str value "%")]
  306. (.setAttribute (rum/deref el-ref) "aria-valuenow" value)
  307. (ui-handler/persist-right-sidebar-width! width))))]
  308. (hooks/use-effect!
  309. (fn []
  310. (when-let [el (and (fn? js/window.interact) (rum/deref el-ref))]
  311. (-> (js/interact el)
  312. (.draggable
  313. (bean/->js
  314. {:listeners
  315. {:move
  316. (fn [^js/MouseEvent e]
  317. (let [width js/document.documentElement.clientWidth
  318. min-ratio (max min-ratio (/ min-px-width width))
  319. sidebar-el (js/document.getElementById sidebar-id)
  320. offset (.-pageX e)
  321. ratio (.toFixed (/ offset width) 6)
  322. ratio (if (= handler-position :west) (- 1 ratio) ratio)
  323. cursor-class (str "cursor-" (first (name handler-position)) "-resize")]
  324. (if (= (.getAttribute el "data-expanded") "true")
  325. (cond
  326. (< ratio (/ min-ratio 2))
  327. (state/hide-right-sidebar!)
  328. (< ratio min-ratio)
  329. (.. js/document.documentElement -classList (add cursor-class))
  330. (and (< ratio max-ratio) sidebar-el)
  331. (when sidebar-el
  332. (#(.. js/document.documentElement -classList (remove cursor-class))
  333. (set-width! ratio)))
  334. :else
  335. #(.. js/document.documentElement -classList (remove cursor-class)))
  336. (when (> ratio (/ min-ratio 2)) (state/open-right-sidebar!)))))}}))
  337. (.styleCursor false)
  338. (.on "dragstart" add-resizing-class)
  339. (.on "dragend" remove-resizing-class)
  340. (.on "keydown" (fn [e]
  341. (when-let [sidebar-el (js/document.getElementById sidebar-id)]
  342. (let [width js/document.documentElement.clientWidth
  343. min-ratio (max min-ratio (/ min-px-width width))
  344. keyboard-step (case (.-code e)
  345. "ArrowLeft" (- keyboard-step)
  346. "ArrowRight" keyboard-step
  347. 0)
  348. offset (+ (.-x (.getBoundingClientRect sidebar-el)) keyboard-step)
  349. ratio (.toFixed (/ offset width) 6)
  350. ratio (if (= handler-position :west) (- 1 ratio) ratio)]
  351. (when (and (> ratio min-ratio) (< ratio max-ratio) (not (zero? keyboard-step)))
  352. (do (add-resizing-class)
  353. (set-width! ratio)))))))
  354. (.on "keyup" remove-resizing-class)))
  355. #())
  356. [])
  357. (hooks/use-effect!
  358. (fn []
  359. ;; sidebar animation duration
  360. (js/setTimeout
  361. #(reset! ui-handler/*right-sidebar-resized-at (js/Date.now)) 300))
  362. [sidebar-open?])
  363. [:.resizer
  364. {:ref el-ref
  365. :role "separator"
  366. :aria-orientation "vertical"
  367. :aria-label (t :right-side-bar/separator)
  368. :aria-valuemin (* min-ratio 100)
  369. :aria-valuemax (* max-ratio 100)
  370. :aria-valuenow 50
  371. :tabIndex "0"
  372. :data-expanded sidebar-open?}]))
  373. (rum/defcs sidebar-inner <
  374. (rum/local false ::anim-finished?)
  375. {:will-mount (fn [state]
  376. (js/setTimeout (fn [] (reset! (get state ::anim-finished?) true)) 300)
  377. state)}
  378. [state repo t blocks]
  379. (let [*anim-finished? (get state ::anim-finished?)
  380. block-count (count blocks)]
  381. [:div.cp__right-sidebar-inner.flex.flex-col.h-full#right-sidebar-container
  382. [:div.cp__right-sidebar-scrollable
  383. {:on-drag-over util/stop}
  384. [:div.cp__right-sidebar-topbar.flex.flex-row.justify-between.items-center
  385. [:div.cp__right-sidebar-settings.hide-scrollbar.gap-1 {:key "right-sidebar-settings"}
  386. [:div.text-sm
  387. [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
  388. (state/sidebar-add-block! repo "contents" :contents))}
  389. (t :right-side-bar/contents)]]
  390. [:div.text-sm
  391. [:button.button.cp__right-sidebar-settings-btn {:on-click (fn []
  392. (when-let [page (get-current-page)]
  393. (state/sidebar-add-block!
  394. repo
  395. page
  396. :page-graph)))}
  397. (t :right-side-bar/page-graph)]]
  398. [:div.text-sm
  399. [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
  400. (state/sidebar-add-block! repo "help" :help))}
  401. (t :right-side-bar/help)]]
  402. (when (and (state/sub [:ui/developer-mode?]) (not config/publishing?))
  403. [:div.text-sm
  404. [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
  405. (state/sidebar-add-block! repo "rtc" :rtc))}
  406. "(Dev) RTC"]])
  407. (when (state/sub [:ui/developer-mode?])
  408. [:div.text-sm
  409. [:button.button.cp__right-sidebar-settings-btn
  410. {:on-click (fn [_e]
  411. (state/sidebar-add-block! repo "vector-search" :vector-search))}
  412. "(Dev) vector-search"]])
  413. (when (state/sub [:ui/developer-mode?])
  414. [:div.text-sm
  415. [:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
  416. (state/sidebar-add-block! repo "profiler" :profiler))}
  417. "(Dev) Profiler"]])]]
  418. [:.sidebar-item-list.flex-1.scrollbar-spacing.px-2
  419. (if @*anim-finished?
  420. (for [[idx [repo db-id block-type]] (medley/indexed blocks)]
  421. (rum/with-key
  422. (sidebar-item repo idx db-id block-type block-count)
  423. (str "sidebar-block-" db-id)))
  424. [:div.p-4
  425. [:span.font-medium.opacity-50 "Loading ..."]])]]]))
  426. (rum/defcs sidebar < rum/reactive
  427. [state]
  428. (let [blocks (state/sub-right-sidebar-blocks)
  429. blocks (if (empty? blocks)
  430. [[(state/get-current-repo) "contents" :contents nil]]
  431. blocks)
  432. sidebar-open? (state/sub :ui/sidebar-open?)
  433. width (state/sub :ui/sidebar-width)
  434. repo (state/sub :git/current-repo)]
  435. [:div#right-sidebar.cp__right-sidebar.h-screen
  436. {:class (if sidebar-open? "open" "closed")
  437. :style {:width width}}
  438. (sidebar-resizer sidebar-open? "right-sidebar" :west)
  439. (when sidebar-open?
  440. (sidebar-inner repo t blocks))]))