search.cljs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. (ns frontend.components.search
  2. (:require [rum.core :as rum]
  3. [lambdaisland.glogi :as log]
  4. [frontend.util :as util]
  5. [frontend.components.block :as block]
  6. [frontend.components.svg :as svg]
  7. [frontend.handler.route :as route]
  8. [frontend.handler.editor :as editor-handler]
  9. [frontend.handler.page :as page-handler]
  10. [frontend.handler.block :as block-handler]
  11. [frontend.handler.notification :as notification]
  12. [frontend.db :as db]
  13. [frontend.db.model :as model]
  14. [frontend.handler.search :as search-handler]
  15. [frontend.handler.whiteboard :as whiteboard-handler]
  16. [frontend.extensions.pdf.assets :as pdf-assets]
  17. [frontend.ui :as ui]
  18. [frontend.state :as state]
  19. [frontend.mixins :as mixins]
  20. [frontend.config :as config]
  21. [clojure.string :as string]
  22. [frontend.context.i18n :refer [t]]
  23. [frontend.date :as date]
  24. [reitit.frontend.easy :as rfe]
  25. [frontend.modules.shortcut.core :as shortcut]))
  26. (defn highlight-exact-query
  27. [content q]
  28. (if (or (string/blank? content) (string/blank? q))
  29. content
  30. (when (and content q)
  31. (let [q-words (string/split q #" ")
  32. lc-content (util/search-normalize content (state/enable-search-remove-accents?))
  33. lc-q (util/search-normalize q (state/enable-search-remove-accents?))]
  34. (if (and (string/includes? lc-content lc-q)
  35. (not (util/safe-re-find #" " q)))
  36. (let [i (string/index-of lc-content lc-q)
  37. [before after] [(subs content 0 i) (subs content (+ i (count q)))]]
  38. [:div
  39. (when-not (string/blank? before)
  40. [:span before])
  41. [:mark.p-0.rounded-none (subs content i (+ i (count q)))]
  42. (when-not (string/blank? after)
  43. [:span after])])
  44. (let [elements (loop [words q-words
  45. content content
  46. result []]
  47. (if (and (seq words) content)
  48. (let [word (first words)
  49. lc-word (util/search-normalize word (state/enable-search-remove-accents?))
  50. lc-content (util/search-normalize content (state/enable-search-remove-accents?))]
  51. (if-let [i (string/index-of lc-content lc-word)]
  52. (recur (rest words)
  53. (subs content (+ i (count word)))
  54. (vec
  55. (concat result
  56. [[:span (subs content 0 i)]
  57. [:mark.p-0.rounded-none (subs content i (+ i (count word)))]])))
  58. (recur nil
  59. content
  60. result)))
  61. (conj result [:span content])))]
  62. [:p {:class "m-0"} elements]))))))
  63. (rum/defc search-result-item
  64. [icon content]
  65. [:.search-result
  66. (ui/type-icon icon)
  67. [:.self-center content]])
  68. (rum/defc block-search-result-item
  69. [repo uuid format content q search-mode]
  70. (let [content (search-handler/sanity-search-content format content)]
  71. [:div
  72. (when (not= search-mode :page)
  73. [:div {:class "mb-1" :key "parents"}
  74. (block/breadcrumb {:id "block-search-block-parent"
  75. :block? true
  76. :search? true}
  77. repo
  78. (clojure.core/uuid uuid)
  79. {:indent? false})])
  80. [:div {:class "font-medium" :key "content"}
  81. (highlight-exact-query content q)]]))
  82. (defonce search-timeout (atom nil))
  83. (defn- search-on-chosen-open-link
  84. [repo search-q {:keys [data type alias]}]
  85. (search-handler/add-search-to-recent! repo search-q)
  86. (search-handler/clear-search!)
  87. (case type
  88. :block
  89. ;; Open the first link in a block's content
  90. (let [block-uuid (uuid (:block/uuid data))
  91. block (:block/content (db/entity [:block/uuid block-uuid]))
  92. link (re-find editor-handler/url-regex block)]
  93. (if link
  94. (js/window.open link)
  95. (notification/show! "No link found on this block." :warning)))
  96. :page
  97. ;; Open the first link found in a page's properties
  98. (let [data (or alias data)
  99. page (when data (db/entity [:block/name (util/page-name-sanity-lc data)]))
  100. link (some #(re-find editor-handler/url-regex (val %)) (:block/properties page))]
  101. (if link
  102. (js/window.open link)
  103. (notification/show! "No link found on this page's properties." :warning)))
  104. nil)
  105. (state/close-modal!))
  106. (defn- search-on-chosen
  107. [repo search-q {:keys [type data alias]}]
  108. (search-handler/add-search-to-recent! repo search-q)
  109. (search-handler/clear-search!)
  110. (case type
  111. :graph-add-filter
  112. (state/add-graph-search-filter! search-q)
  113. :new-page
  114. (page-handler/create! search-q {:redirect? true})
  115. :new-whiteboard
  116. (whiteboard-handler/create-new-whiteboard! search-q)
  117. :page
  118. (let [data (or alias data)]
  119. (cond
  120. (model/whiteboard-page? data)
  121. (route/redirect-to-whiteboard! data)
  122. :else
  123. (route/redirect-to-page! data)))
  124. :file
  125. (route/redirect! {:to :file
  126. :path-params {:path data}})
  127. :block
  128. (let [block-uuid (uuid (:block/uuid data))
  129. collapsed? (db/parents-collapsed? repo block-uuid)
  130. page (:block/page (db/entity [:block/uuid block-uuid]))
  131. page-name (:block/name page)
  132. long-page? (block-handler/long-page? repo (:db/id page))]
  133. (if page
  134. (cond
  135. (model/whiteboard-page? page-name)
  136. (route/redirect-to-whiteboard! page-name {:block-id block-uuid})
  137. (or collapsed? long-page?)
  138. (route/redirect-to-page! block-uuid)
  139. :else
  140. (route/redirect-to-page! (:block/name page) {:anchor (str "ls-block-" (:block/uuid data))}))
  141. ;; search indice outdated
  142. (println "[Error] Block page missing: "
  143. {:block-id block-uuid
  144. :block (db/pull [:block/uuid block-uuid])})))
  145. nil)
  146. (state/close-modal!))
  147. (defn- search-on-shift-chosen
  148. [repo search-q {:keys [type data alias]}]
  149. (search-handler/add-search-to-recent! repo search-q)
  150. (case type
  151. :page
  152. (let [data (or alias data)
  153. page (when data (db/entity [:block/name (util/page-name-sanity-lc data)]))]
  154. (when page
  155. (state/sidebar-add-block!
  156. repo
  157. (:db/id page)
  158. :page)))
  159. :block
  160. (let [block-uuid (uuid (:block/uuid data))
  161. block (db/entity [:block/uuid block-uuid])]
  162. (state/sidebar-add-block!
  163. repo
  164. (:db/id block)
  165. :block))
  166. :new-page
  167. (page-handler/create! search-q)
  168. :file
  169. (route/redirect! {:to :file
  170. :path-params {:path data}})
  171. nil)
  172. (state/close-modal!))
  173. (defn- create-item-render
  174. [icon label name]
  175. (search-result-item
  176. {:name icon
  177. :class "highlight"
  178. :extension? true}
  179. [:div.text.font-bold (str label ": ")
  180. [:span.ml-1 name]]))
  181. (defn- search-item-render
  182. [search-q {:keys [type data alias]}]
  183. (let [search-mode (state/get-search-mode)
  184. data (if (string? data) (pdf-assets/fix-local-asset-filename data) data)]
  185. [:div {:class "py-2"}
  186. (case type
  187. :graph-add-filter
  188. [:b search-q]
  189. :new-page
  190. (create-item-render "new-page" (t :new-page) (str "\"" (string/trim search-q) "\""))
  191. :new-whiteboard
  192. (create-item-render "new-whiteboard" (t :new-whiteboard) (str "\"" (string/trim search-q) "\""))
  193. :page
  194. [:span {:data-page-ref data}
  195. (when alias
  196. (let [target-original-name (model/get-page-original-name alias)]
  197. [:span.mr-2.text-sm.font-medium.mb-2 (str "Alias -> " target-original-name)]))
  198. (search-result-item {:name (if (model/whiteboard-page? data) "whiteboard" "page")
  199. :extension? true
  200. :title (t (if (model/whiteboard-page? data) :search-item/whiteboard :search-item/page))}
  201. (highlight-exact-query data search-q))]
  202. :file
  203. (search-result-item {:name "file"
  204. :title (t :search-item/file)}
  205. (highlight-exact-query data search-q))
  206. :block
  207. (let [{:block/keys [page uuid]} data ;; content here is normalized
  208. page (util/get-page-original-name page)
  209. repo (state/sub :git/current-repo)
  210. format (db/get-page-format page)
  211. block (model/query-block-by-uuid uuid)
  212. content (:block/content block)]
  213. [:span {:data-block-ref uuid}
  214. (search-result-item {:name "block"
  215. :title (t :search-item/block)
  216. :extension? true}
  217. (if block
  218. (block-search-result-item repo uuid format content search-q search-mode)
  219. (do (log/error "search result with non-existing uuid: " data)
  220. (str "Cache is outdated. Please click the 'Re-index' button in the graph's dropdown menu."))))])
  221. nil)]))
  222. (rum/defc search-auto-complete
  223. [{:keys [pages files blocks has-more?] :as result} search-q all?]
  224. (let [pages (when-not all? (map (fn [page]
  225. (let [alias (model/get-redirect-page-name page)]
  226. (cond->
  227. {:type :page
  228. :data page}
  229. (and alias
  230. (not= (util/page-name-sanity-lc page)
  231. (util/page-name-sanity-lc alias)))
  232. (assoc :alias alias))))
  233. (remove nil? pages)))
  234. files (when-not all? (map (fn [file] {:type :file :data file}) files))
  235. blocks (map (fn [block] {:type :block :data block}) blocks)
  236. search-mode (state/sub :search/mode)
  237. new-page (if (or
  238. (and (seq pages)
  239. (= (util/safe-page-name-sanity-lc search-q)
  240. (util/safe-page-name-sanity-lc (:data (first pages)))))
  241. (nil? result)
  242. all?)
  243. []
  244. (if (state/enable-whiteboards?)
  245. [{:type :new-page} {:type :new-whiteboard}]
  246. [{:type :new-page}]))
  247. result (cond
  248. config/publishing?
  249. (concat pages files blocks)
  250. (= :whiteboard/link search-mode)
  251. (concat pages blocks)
  252. :else
  253. (concat new-page pages files blocks))
  254. result (if (= search-mode :graph)
  255. [{:type :graph-add-filter}]
  256. result)
  257. repo (state/get-current-repo)]
  258. [:div
  259. (ui/auto-complete
  260. result
  261. {:class "search-results"
  262. :on-chosen #(search-on-chosen repo search-q %)
  263. :on-shift-chosen #(search-on-shift-chosen repo search-q %)
  264. :item-render #(search-item-render search-q %)
  265. :on-chosen-open-link #(search-on-chosen-open-link repo search-q %)})
  266. (when (and has-more? (util/electron?) (not all?))
  267. [:div.px-2.py-4.search-more
  268. [:a.text-sm.font-medium {:href (rfe/href :search {:q search-q})
  269. :on-click (fn []
  270. (when-not (string/blank? search-q)
  271. (state/close-modal!)
  272. (search-handler/search (state/get-current-repo) search-q {:limit 1000
  273. :more? true})
  274. (search-handler/clear-search!)))}
  275. (t :more)]])]))
  276. (rum/defc recent-search-and-pages
  277. [in-page-search?]
  278. [:div.recent-search
  279. [:div.wrap.px-4.py-2.text-sm.opacity-70.flex.flex-row.justify-between.align-items.mx-1.sm:mx-0
  280. [:div "Recent search:"]
  281. (ui/with-shortcut :go/search-in-page "bottom"
  282. [:div.flex-row.flex.align-items
  283. [:div.mr-3.flex "Search blocks in page:"]
  284. [:div.flex.items-center
  285. (ui/toggle in-page-search?
  286. (fn [_value]
  287. (state/set-search-mode! (if in-page-search? :global :page)))
  288. true)]
  289. (ui/tippy {:html [:div
  290. ;; TODO: fetch from config
  291. "Tip: " [:code (util/->platform-shortcut "Ctrl + Shift + p")] " to open the commands palette"]
  292. :interactive true
  293. :arrow true
  294. :theme "monospace"}
  295. [:a.flex.fade-link.items-center
  296. {:style {:margin-left 12}
  297. :on-click #(state/toggle! :ui/command-palette-open?)}
  298. (ui/icon "command" {:style {:font-size 20}})])])]
  299. (let [recent-search (mapv (fn [q] {:type :search :data q}) (db/get-key-value :recent/search))
  300. pages (->> (db/get-key-value :recent/pages)
  301. (remove nil?)
  302. (filter string?)
  303. (remove #(= (string/lower-case %) "contents"))
  304. (mapv (fn [page] {:type :page :data page})))
  305. result (concat (take 5 recent-search) pages)]
  306. (ui/auto-complete
  307. result
  308. {:on-chosen (fn [{:keys [type data]}]
  309. (case type
  310. :page
  311. (do (route/redirect-to-page! data)
  312. (state/close-modal!))
  313. :search
  314. (let [q data]
  315. (state/set-q! q)
  316. (let [search-mode (state/get-search-mode)
  317. opts (if (= :page search-mode)
  318. (let [current-page (or (state/get-current-page)
  319. (date/today))]
  320. {:page-db-id (:db/id (db/entity [:block/name (util/page-name-sanity-lc current-page)]))})
  321. {})]
  322. (if (= :page search-mode)
  323. (search-handler/search (state/get-current-repo) q opts)
  324. (search-handler/search (state/get-current-repo) q))))
  325. nil))
  326. :on-shift-chosen (fn [{:keys [type data]}]
  327. (case type
  328. :page
  329. (let [page data]
  330. (when (string? page)
  331. (when-let [page (db/pull [:block/name (util/page-name-sanity-lc page)])]
  332. (state/sidebar-add-block!
  333. (state/get-current-repo)
  334. (:db/id page)
  335. :page))
  336. (state/close-modal!)))
  337. nil))
  338. :item-render (fn [{:keys [type data]}]
  339. (case type
  340. :search [:div.flex-row.flex.search-item.font-medium
  341. svg/search
  342. [:span.ml-2 data]]
  343. :page (when-let [original-name (model/get-page-original-name data)] ;; might be block reference
  344. (search-result-item {:name "page"
  345. :extension? true}
  346. original-name))
  347. nil))}))])
  348. (defn default-placeholder
  349. [search-mode]
  350. (cond
  351. config/publishing?
  352. (t :search/publishing)
  353. (= search-mode :whiteboard/link)
  354. (t :whiteboard/link-whiteboard-or-block)
  355. :else
  356. (t :search)))
  357. (rum/defcs search-modal < rum/reactive
  358. (shortcut/disable-all-shortcuts)
  359. (mixins/event-mixin
  360. (fn [state]
  361. (mixins/hide-when-esc-or-outside
  362. state
  363. :on-hide (fn []
  364. (search-handler/clear-search!)))))
  365. [state]
  366. (let [search-result (state/sub :search/result)
  367. search-q (state/sub :search/q)
  368. search-mode (state/sub :search/mode)
  369. timeout 300
  370. in-page-search? (= search-mode :page)]
  371. [:div.cp__palette.cp__palette-main
  372. [:div.ls-search
  373. [:div.input-wrap
  374. [:input.cp__palette-input.w-full
  375. {:type "text"
  376. :auto-focus true
  377. :placeholder (case search-mode
  378. :graph
  379. (t :graph-search)
  380. :page
  381. (t :page-search)
  382. (default-placeholder search-mode))
  383. :auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here
  384. :value search-q
  385. :on-change (fn [e]
  386. (when @search-timeout
  387. (js/clearTimeout @search-timeout))
  388. (let [value (util/evalue e)
  389. is-composing? (util/onchange-event-is-composing? e)] ;; #3199
  390. (if (and (string/blank? value) (not is-composing?))
  391. (search-handler/clear-search! false)
  392. (let [search-mode (state/get-search-mode)
  393. opts (if (= :page search-mode)
  394. (when-let [current-page (or (state/get-current-page)
  395. (date/today))]
  396. {:page-db-id (:db/id (db/entity [:block/name (util/page-name-sanity-lc current-page)]))})
  397. {})]
  398. (state/set-q! value)
  399. (reset! search-timeout
  400. (js/setTimeout
  401. (fn []
  402. (if (= :page search-mode)
  403. (search-handler/search (state/get-current-repo) value opts)
  404. (search-handler/search (state/get-current-repo) value)))
  405. timeout))))))}]]
  406. [:div.search-results-wrap
  407. (if (seq search-result)
  408. (search-auto-complete search-result search-q false)
  409. (recent-search-and-pages in-page-search?))]]]))
  410. (rum/defc more < rum/reactive
  411. [route]
  412. (let [search-q (get-in route [:path-params :q])
  413. search-result (state/sub :search/more-result)]
  414. [:div#search.flex-1.flex
  415. [:div.inner
  416. [:h1.title (t :search/result-for) [:i search-q]]
  417. [:p.font-medium.tx-sm (str (count (:blocks search-result)) " " (t :search/items))]
  418. [:div#search-wrapper.relative.w-full.text-gray-400.focus-within:text-gray-600
  419. (when-not (string/blank? search-q)
  420. (search-auto-complete search-result search-q true))]]]))