search.cljs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  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.components.search.highlight :as highlight]
  8. [frontend.handler.route :as route-handler]
  9. [frontend.handler.editor :as editor-handler]
  10. [frontend.handler.property :as property-handler]
  11. [frontend.handler.page :as page-handler]
  12. [frontend.handler.notification :as notification]
  13. [frontend.db :as db]
  14. [frontend.db.model :as model]
  15. [frontend.handler.search :as search-handler]
  16. [frontend.handler.whiteboard :as whiteboard-handler]
  17. [frontend.handler.recent :as recent-handler]
  18. [frontend.extensions.pdf.utils :as pdf-utils]
  19. [frontend.ui :as ui]
  20. [frontend.state :as state]
  21. [frontend.mixins :as mixins]
  22. [frontend.config :as config]
  23. [clojure.string :as string]
  24. [frontend.context.i18n :refer [t]]
  25. [frontend.date :as date]
  26. [reitit.frontend.easy :as rfe]
  27. [frontend.modules.shortcut.core :as shortcut]
  28. [frontend.util.text :as text-util]))
  29. (defn highlight-page-content-query
  30. "Return hiccup of highlighted page content FTS result"
  31. [content q]
  32. (when-not (or (string/blank? content) (string/blank? q))
  33. [:div (loop [content content ;; why recur? because there might be multiple matches
  34. result []]
  35. (let [[b-cut hl-cut e-cut] (text-util/cut-by content "$pfts_2lqh>$" "$<pfts_2lqh$")
  36. hiccups-add [(when-not (string/blank? b-cut)
  37. [:span b-cut])
  38. (when-not (string/blank? hl-cut)
  39. [:mark.p-0.rounded-none hl-cut])]
  40. hiccups-add (remove nil? hiccups-add)
  41. new-result (concat result hiccups-add)]
  42. (if-not (string/blank? e-cut)
  43. (recur e-cut new-result)
  44. new-result)))]))
  45. (rum/defc search-result-item
  46. [icon content]
  47. [:.search-result
  48. (ui/type-icon icon)
  49. [:.self-center content]])
  50. (rum/defc page-content-search-result-item
  51. [repo uuid format snippet q search-mode]
  52. [:div
  53. (when (not= search-mode :page)
  54. [:div {:class "mb-1" :key "parents"}
  55. (block/breadcrumb {:id "block-search-block-parent"
  56. :block? true
  57. :search? true}
  58. repo
  59. (clojure.core/uuid uuid)
  60. {:indent? false})])
  61. [:div {:class "font-medium" :key "content"}
  62. (highlight-page-content-query (search-handler/sanity-search-content format snippet) q)]])
  63. (rum/defc block-search-result-item
  64. [repo uuid format content q search-mode]
  65. (let [content (search-handler/sanity-search-content format content)]
  66. [:div
  67. (when (not= search-mode :page)
  68. [:div {:class "mb-1" :key "parents"}
  69. (block/breadcrumb {:id "block-search-block-parent"
  70. :block? true
  71. :search? true}
  72. repo
  73. (clojure.core/uuid uuid)
  74. {:indent? false})])
  75. [:div {:class "font-medium" :key "content"}
  76. (highlight/highlight-exact-query content q)]]))
  77. (defonce search-timeout (atom nil))
  78. (defn- search-on-chosen-open-link
  79. [repo search-q {:keys [data type alias]}]
  80. (search-handler/add-search-to-recent! repo search-q)
  81. (search-handler/clear-search!)
  82. (case type
  83. :block
  84. ;; Open the first link in a block's content
  85. (let [block-uuid (uuid (:block/uuid data))
  86. block (:block/content (db/entity [:block/uuid block-uuid]))
  87. link (re-find editor-handler/url-regex block)]
  88. (if link
  89. (js/window.open link)
  90. (notification/show! "No link found on this block." :warning)))
  91. :page
  92. ;; Open the first link found in a page's properties
  93. (let [data (or alias data)
  94. page (when data (db/entity [:block/name (util/page-name-sanity-lc data)]))
  95. link (some #(re-find editor-handler/url-regex (val %)) (:block/properties page))]
  96. (if link
  97. (js/window.open link)
  98. (notification/show! "No link found on this page's properties." :warning)))
  99. nil)
  100. (state/close-modal!))
  101. (defn- search-on-chosen
  102. [repo search-q {:keys [type data alias]}]
  103. (search-handler/add-search-to-recent! repo search-q)
  104. (search-handler/clear-search!)
  105. (case type
  106. :graph-add-filter
  107. (state/add-graph-search-filter! search-q)
  108. :new-page
  109. (page-handler/create! search-q {:redirect? true})
  110. :new-class
  111. (let [search-q' (subs search-q 1)]
  112. (page-handler/create! search-q' {:class? true
  113. :redirect? false})
  114. (state/pub-event! [:class/configure (db/entity [:block/name (util/page-name-sanity-lc search-q')]) {}]))
  115. :new-whiteboard
  116. (whiteboard-handler/create-new-whiteboard-and-redirect! search-q)
  117. :page
  118. (let [data (or alias data)]
  119. (cond
  120. (model/whiteboard-page? data)
  121. (route-handler/redirect-to-whiteboard! data)
  122. :else
  123. (route-handler/redirect-to-page! data)))
  124. :file
  125. (route-handler/redirect! {:to :file
  126. :path-params {:path data}})
  127. :block
  128. (let [block-uuid (uuid (:block/uuid data))
  129. block-uuid (or
  130. (some-> (property-handler/get-property-block-created-block [:block/uuid block-uuid])
  131. db/entity
  132. :block/uuid)
  133. block-uuid)
  134. collapsed? (db/parents-collapsed? repo block-uuid)
  135. page (:block/page (db/entity [:block/uuid block-uuid]))
  136. page-name (:block/name page)]
  137. (if page
  138. (cond
  139. (model/whiteboard-page? page-name)
  140. (route-handler/redirect-to-whiteboard! page-name {:block-id block-uuid})
  141. collapsed?
  142. (route-handler/redirect-to-page! block-uuid)
  143. :else
  144. (route-handler/redirect-to-page! (:block/name page) {:anchor (str "ls-block-" (:block/uuid data))}))
  145. ;; search indice outdated
  146. (println "[Error] Block page missing: "
  147. {:block-id block-uuid
  148. :block (db/pull [:block/uuid block-uuid])})))
  149. :page-content
  150. (let [page-uuid (uuid (:block/uuid data))
  151. page (model/get-block-by-uuid page-uuid)
  152. page-name (:block/name page)]
  153. (if page
  154. (cond
  155. (model/whiteboard-page? page-name)
  156. (route-handler/redirect-to-whiteboard! page-name)
  157. :else
  158. (route-handler/redirect-to-page! page-name))
  159. ;; search indice outdated
  160. (println "[Error] page missing: "
  161. {:page-uuid page-uuid
  162. :page page})))
  163. nil)
  164. (state/close-modal!))
  165. (defn- search-on-shift-chosen
  166. [repo search-q {:keys [type data alias]}]
  167. (search-handler/add-search-to-recent! repo search-q)
  168. (case type
  169. :page
  170. (let [data (or alias data)
  171. page (when data (db/entity [:block/name (util/page-name-sanity-lc data)]))]
  172. (when page
  173. (state/sidebar-add-block!
  174. repo
  175. (:db/id page)
  176. :page)))
  177. :page-content
  178. (let [page-uuid (uuid (:block/uuid data))
  179. page (model/get-block-by-uuid page-uuid)]
  180. (if page
  181. (state/sidebar-add-block!
  182. repo
  183. (:db/id page)
  184. :page)
  185. ;; search indice outdated
  186. (println "[Error] page missing: "
  187. {:page-uuid page-uuid
  188. :page page})))
  189. :block
  190. (let [block-uuid (uuid (:block/uuid data))
  191. block (db/entity [:block/uuid block-uuid])]
  192. (state/sidebar-add-block!
  193. repo
  194. (:db/id block)
  195. :block))
  196. :new-page
  197. (page-handler/create! search-q)
  198. :new-class
  199. (page-handler/create! search-q {:class? true
  200. :redirect? false})
  201. :file
  202. (route-handler/redirect! {:to :file
  203. :path-params {:path data}})
  204. nil)
  205. (state/close-modal!))
  206. (defn- create-item-render
  207. [icon label name]
  208. (search-result-item
  209. {:name icon
  210. :class "highlight"
  211. :extension? true}
  212. [:div.text.font-bold label
  213. [:span.ml-2 name]]))
  214. (defn- search-item-render
  215. [search-q {:keys [type data alias]}]
  216. (let [search-mode (state/get-search-mode)
  217. data (if (string? data) (pdf-utils/fix-local-asset-pagename data) data)]
  218. [:div {:class "py-2"}
  219. (case type
  220. :graph-add-filter
  221. [:b search-q]
  222. :new-page
  223. (create-item-render "new-page" (t :new-page) (str "\"" (string/trim search-q) "\""))
  224. :new-class
  225. ;; TODO: Add icon for new-class
  226. (create-item-render "new-page" (t :new-class) (str "\"" (string/trim (subs search-q 1)) "\""))
  227. :new-whiteboard
  228. (create-item-render "new-whiteboard" (t :new-whiteboard) (str "\"" (string/trim search-q) "\""))
  229. :page
  230. [:span {:data-page-ref data}
  231. (when alias
  232. (let [target-original-name (model/get-page-original-name alias)]
  233. [:span.mr-2.text-sm.font-medium.mb-2 (str "Alias -> " target-original-name)]))
  234. (search-result-item {:name (if (model/whiteboard-page? data) "whiteboard" "page")
  235. :extension? true
  236. :title (t (if (model/whiteboard-page? data) :search-item/whiteboard :search-item/page))}
  237. (highlight/highlight-exact-query data search-q))]
  238. :file
  239. (search-result-item {:name "file"
  240. :title (t :search-item/file)}
  241. (highlight/highlight-exact-query data search-q))
  242. :block
  243. (let [{:block/keys [page uuid content]} data ;; content here is normalized
  244. page (util/get-page-original-name page)
  245. repo (state/sub :git/current-repo)
  246. format (db/get-page-format page)
  247. block (when-not (string/blank? uuid)
  248. (model/query-block-by-uuid uuid))
  249. content' (if block (:block/content block) content)]
  250. [:span {:data-block-ref uuid}
  251. (search-result-item {:name "block"
  252. :title (t :search-item/block)
  253. :extension? true}
  254. (cond
  255. (some? block)
  256. (block-search-result-item repo uuid format content' search-q search-mode)
  257. (not (string/blank? content'))
  258. content'
  259. :else
  260. (do (log/error "search result with non-existing uuid: " data)
  261. (t :search/cache-outdated))))])
  262. :page-content
  263. (let [{:block/keys [snippet uuid]} data ;; content here is normalized
  264. repo (state/sub :git/current-repo)
  265. page (when uuid (model/query-block-by-uuid uuid)) ;; it's actually a page
  266. format (db/get-page-format page)]
  267. (when page
  268. [:span {:data-block-ref uuid}
  269. (search-result-item {:name "page"
  270. :title (t :search-item/page)
  271. :extension? true}
  272. (if page
  273. (page-content-search-result-item repo uuid format snippet search-q search-mode)
  274. (do (log/error "search result with non-existing uuid: " data)
  275. (t :search/cache-outdated))))]))
  276. nil)]))
  277. (rum/defc search-auto-complete
  278. "has-more? - if the result is truncated
  279. all? - if true, in show-more mode"
  280. [{:keys [engine pages files pages-content blocks has-more?] :as result} search-q all?]
  281. (let [pages (when-not all? (map (fn [page]
  282. (let [alias (model/get-redirect-page-name page)]
  283. (cond->
  284. {:type :page
  285. :data page}
  286. (and alias
  287. (not= (util/page-name-sanity-lc page)
  288. (util/page-name-sanity-lc alias)))
  289. (assoc :alias alias))))
  290. (remove nil? pages)))
  291. files (when-not all? (map (fn [file] {:type :file :data file}) files))
  292. blocks (map (fn [block] {:type :block :data block}) blocks)
  293. pages-content (map (fn [pages-content] {:type :page-content :data pages-content}) pages-content)
  294. search-mode (state/sub :search/mode)
  295. tag-search? (= \# (first search-q))
  296. new-page (if (or
  297. (some? engine)
  298. (let [search-q' (util/safe-page-name-sanity-lc search-q)
  299. first-matched-item (util/safe-page-name-sanity-lc (:data (first pages)))]
  300. (and (seq pages)
  301. (or (= search-q' first-matched-item)
  302. (and tag-search? (= search-q' (str "#" first-matched-item))))))
  303. (nil? result)
  304. all?)
  305. []
  306. (if (state/enable-whiteboards?)
  307. [{:type (if tag-search? :new-class :new-page)} {:type :new-whiteboard}]
  308. [{:type :new-page}]))
  309. result (cond
  310. config/publishing?
  311. (concat pages files blocks) ;; Browser doesn't have page content FTS
  312. (= :whiteboard/link search-mode)
  313. (concat pages blocks pages-content)
  314. :else
  315. (concat new-page pages files blocks pages-content))
  316. result (if (= search-mode :graph)
  317. [{:type :graph-add-filter}]
  318. result)
  319. repo (state/get-current-repo)]
  320. [:div.results-inner
  321. (ui/auto-complete
  322. result
  323. {:class "search-results"
  324. :on-chosen #(search-on-chosen repo search-q %)
  325. :on-shift-chosen #(search-on-shift-chosen repo search-q %)
  326. :item-render #(search-item-render search-q %)
  327. :on-chosen-open-link #(search-on-chosen-open-link repo search-q %)})
  328. (when (and has-more? (not all?))
  329. [:div.px-2.py-4.search-more
  330. [:a.text-sm.font-medium {:href (rfe/href :search {:q search-q})
  331. :on-click (fn []
  332. (when-not (string/blank? search-q)
  333. (state/close-modal!)
  334. (search-handler/search (state/get-current-repo) search-q {:limit 1000
  335. :more? true})
  336. (search-handler/clear-search!)))}
  337. (t :more)]])]))
  338. (rum/defc recent-search-and-pages
  339. [in-page-search?]
  340. [:div.recent-search
  341. [:div.wrap.px-4.pb-2.text-sm.opacity-70.flex.flex-row.justify-between.align-items.mx-1.sm:mx-0
  342. [:div (t :search/recent)]
  343. [:div.hidden.md:flex
  344. (ui/with-shortcut :go/search-in-page "bottom"
  345. [:div.flex-row.flex.align-items
  346. [:div.mr-3.flex (t :search/blocks-in-page)]
  347. [:div.flex.items-center
  348. (ui/toggle in-page-search?
  349. (fn [_value]
  350. (state/set-search-mode! (if in-page-search? :global :page)))
  351. true)]
  352. (ui/tippy {:html [:div
  353. ;; TODO: fetch from config
  354. (t :search/command-palette-tip-1) [:code (util/->platform-shortcut "Ctrl + Shift + p")] (t :search/command-palette-tip-2)]
  355. :interactive true
  356. :arrow true
  357. :theme "monospace"}
  358. [:a.flex.fade-link.items-center
  359. {:style {:margin-left 12}
  360. :on-click #(state/pub-event! [:modal/command-palette])}
  361. (ui/icon "command" {:style {:font-size 20}})])])]]
  362. (let [recent-search (mapv (fn [q] {:type :search :data q})
  363. (if (config/db-based-graph? (state/get-current-repo))
  364. (state/get-recent-search)
  365. (db/get-key-value :recent/search)))
  366. pages (->> (recent-handler/get-recent-pages)
  367. (mapv (fn [page] {:type :page :data page})))
  368. result (concat (take 5 recent-search) pages)]
  369. (ui/auto-complete
  370. result
  371. {:on-chosen (fn [{:keys [type data]}]
  372. (case type
  373. :page
  374. (do (route-handler/redirect-to-page! data)
  375. (state/close-modal!))
  376. :search
  377. (let [q data]
  378. (state/set-q! q)
  379. (let [search-mode (state/get-search-mode)
  380. opts (if (= :page search-mode)
  381. (let [current-page (or (state/get-current-page)
  382. (date/today))]
  383. {:page-db-id (:db/id (db/entity [:block/name (util/page-name-sanity-lc current-page)]))})
  384. {})]
  385. (if (= :page search-mode)
  386. (search-handler/search (state/get-current-repo) q opts)
  387. (search-handler/search (state/get-current-repo) q))))
  388. nil))
  389. :on-shift-chosen (fn [{:keys [type data]}]
  390. (case type
  391. :page
  392. (let [page data]
  393. (when (string? page)
  394. (when-let [page (db/pull [:block/name (util/page-name-sanity-lc page)])]
  395. (state/sidebar-add-block!
  396. (state/get-current-repo)
  397. (:db/id page)
  398. :page))
  399. (state/close-modal!)))
  400. nil))
  401. :item-render (fn [{:keys [type data]}]
  402. (case type
  403. :search [:div.flex-row.flex.search-item.font-medium
  404. svg/search
  405. [:span.ml-2 data]]
  406. :page (when-let [original-name (model/get-page-original-name data)] ;; might be block reference
  407. (search-result-item {:name "page"
  408. :extension? true}
  409. original-name))
  410. nil))}))])
  411. (defn default-placeholder
  412. [search-mode]
  413. (cond
  414. config/publishing?
  415. (t :search/publishing)
  416. (= search-mode :whiteboard/link)
  417. (t :whiteboard/link-whiteboard-or-block)
  418. :else
  419. (t :search)))
  420. (rum/defcs search-modal < rum/reactive
  421. shortcut/disable-all-shortcuts
  422. (mixins/event-mixin
  423. (fn [state]
  424. (mixins/hide-when-esc-or-outside
  425. state
  426. :on-hide (fn []
  427. (search-handler/clear-search!)))))
  428. (rum/local nil ::active-engine-tab)
  429. [state]
  430. (let [search-result (state/sub :search/result)
  431. search-q (state/sub :search/q)
  432. search-mode (state/sub :search/mode)
  433. engines (state/sub :search/engines)
  434. *active-engine-tab (::active-engine-tab state)
  435. timeout 300
  436. in-page-search? (= search-mode :page)]
  437. [:div.cp__palette.cp__palette-main
  438. [:div.ls-search.p-2.md:p-0
  439. [:div.input-wrap
  440. [:input.cp__palette-input.w-full.h-full
  441. {:type "text"
  442. :auto-focus true
  443. :placeholder (case search-mode
  444. :graph
  445. (t :graph-search)
  446. :page
  447. (t :page-search)
  448. (default-placeholder search-mode))
  449. :auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here
  450. :value search-q
  451. :on-key-down (fn [^js e]
  452. (when (= 27 (.-keyCode e))
  453. (when-not (string/blank? search-q)
  454. (util/stop e)
  455. (search-handler/clear-search!))))
  456. :on-change (fn [^js e]
  457. (when @search-timeout
  458. (js/clearTimeout @search-timeout))
  459. (let [value (util/evalue e)
  460. is-composing? (util/onchange-event-is-composing? e)] ;; #3199
  461. (if (and (string/blank? value) (not is-composing?))
  462. (search-handler/clear-search! false)
  463. (let [search-mode (state/get-search-mode)
  464. opts (if (= :page search-mode)
  465. (when-let [current-page (or (state/get-current-page)
  466. (date/today))]
  467. {:page-db-id (:db/id (db/entity [:block/name (util/page-name-sanity-lc current-page)]))})
  468. {})]
  469. (state/set-q! value)
  470. (reset! search-timeout
  471. (js/setTimeout
  472. (fn []
  473. (if (= :page search-mode)
  474. (search-handler/search (state/get-current-repo) value opts)
  475. (search-handler/search (state/get-current-repo) value)))
  476. timeout))))))}]]
  477. [:div.search-results-wrap
  478. ;; list registered search engines
  479. (when (seq engines)
  480. [:ul.search-results-engines-tabs
  481. [:li
  482. {:class (when-not @*active-engine-tab "is-active")}
  483. (ui/button
  484. [:span.flex.items-center
  485. (svg/logo 14) [:span.pl-2 "Default"]]
  486. :background "orange"
  487. :on-click #(reset! *active-engine-tab nil))]
  488. (for [[k v] engines]
  489. [:li
  490. {:key k
  491. :class (if (= k @*active-engine-tab) "is-active" "")}
  492. (ui/button [:span.flex.items-center
  493. [:span.pr-2 (ui/icon "puzzle")]
  494. (:name v)
  495. (when-let [result (and v (:result v))]
  496. (str " (" (apply + (map count ((juxt :blocks :pages :files) result))) ")"))]
  497. :on-click #(reset! *active-engine-tab k))])])
  498. (if-not (nil? @*active-engine-tab)
  499. (let [active-engine-result (get-in engines [@*active-engine-tab :result])]
  500. (search-auto-complete
  501. (merge active-engine-result {:engine @*active-engine-tab}) search-q false))
  502. (if (seq search-result)
  503. (search-auto-complete search-result search-q false)
  504. (recent-search-and-pages in-page-search?)))]]]))
  505. (rum/defc more < rum/reactive
  506. [route]
  507. (let [search-q (get-in route [:path-params :q])
  508. search-result (state/sub :search/more-result)]
  509. [:div#search.flex-1.flex
  510. [:div.inner
  511. [:h1.title (t :search/result-for) [:i search-q]]
  512. [:p.font-medium.tx-sm (str (count (:blocks search-result)) " " (t :search/items))]
  513. [:div#search-wrapper.relative.w-full.text-gray-400.focus-within:text-gray-600
  514. (when-not (string/blank? search-q)
  515. (search-auto-complete search-result search-q true))]]]))