| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566 |
- (ns frontend.components.search
- (:require [rum.core :as rum]
- [lambdaisland.glogi :as log]
- [frontend.util :as util]
- [frontend.components.block :as block]
- [frontend.components.svg :as svg]
- [frontend.components.search.highlight :as highlight]
- [frontend.handler.route :as route-handler]
- [frontend.handler.editor :as editor-handler]
- [frontend.handler.property :as property-handler]
- [frontend.handler.page :as page-handler]
- [frontend.handler.notification :as notification]
- [frontend.db :as db]
- [frontend.db.model :as model]
- [frontend.handler.search :as search-handler]
- [frontend.handler.whiteboard :as whiteboard-handler]
- [frontend.handler.recent :as recent-handler]
- [frontend.extensions.pdf.utils :as pdf-utils]
- [frontend.ui :as ui]
- [frontend.state :as state]
- [frontend.mixins :as mixins]
- [frontend.config :as config]
- [clojure.string :as string]
- [frontend.context.i18n :refer [t]]
- [frontend.date :as date]
- [reitit.frontend.easy :as rfe]
- [frontend.modules.shortcut.core :as shortcut]
- [frontend.util.text :as text-util]))
- (defn highlight-page-content-query
- "Return hiccup of highlighted page content FTS result"
- [content q]
- (when-not (or (string/blank? content) (string/blank? q))
- [:div (loop [content content ;; why recur? because there might be multiple matches
- result []]
- (let [[b-cut hl-cut e-cut] (text-util/cut-by content "$pfts_2lqh>$" "$<pfts_2lqh$")
- hiccups-add [(when-not (string/blank? b-cut)
- [:span b-cut])
- (when-not (string/blank? hl-cut)
- [:mark.p-0.rounded-none hl-cut])]
- hiccups-add (remove nil? hiccups-add)
- new-result (concat result hiccups-add)]
- (if-not (string/blank? e-cut)
- (recur e-cut new-result)
- new-result)))]))
- (rum/defc search-result-item
- [icon content]
- [:.search-result
- (ui/type-icon icon)
- [:.self-center content]])
- (rum/defc page-content-search-result-item
- [repo uuid format snippet q search-mode]
- [:div
- (when (not= search-mode :page)
- [:div {:class "mb-1" :key "parents"}
- (block/breadcrumb {:id "block-search-block-parent"
- :block? true
- :search? true}
- repo
- (clojure.core/uuid uuid)
- {:indent? false})])
- [:div {:class "font-medium" :key "content"}
- (highlight-page-content-query (search-handler/sanity-search-content format snippet) q)]])
- (rum/defc block-search-result-item
- [repo uuid format content q search-mode]
- (let [content (search-handler/sanity-search-content format content)]
- [:div
- (when (not= search-mode :page)
- [:div {:class "mb-1" :key "parents"}
- (block/breadcrumb {:id "block-search-block-parent"
- :block? true
- :search? true}
- repo
- (clojure.core/uuid uuid)
- {:indent? false})])
- [:div {:class "font-medium" :key "content"}
- (highlight/highlight-exact-query content q)]]))
- (defonce search-timeout (atom nil))
- (defn- search-on-chosen-open-link
- [repo search-q {:keys [data type alias]}]
- (search-handler/add-search-to-recent! repo search-q)
- (search-handler/clear-search!)
- (case type
- :block
- ;; Open the first link in a block's content
- (let [block-uuid (uuid (:block/uuid data))
- block (:block/content (db/entity [:block/uuid block-uuid]))
- link (re-find editor-handler/url-regex block)]
- (if link
- (js/window.open link)
- (notification/show! "No link found on this block." :warning)))
- :page
- ;; Open the first link found in a page's properties
- (let [data (or alias data)
- page (when data (db/entity [:block/name (util/page-name-sanity-lc data)]))
- link (some #(re-find editor-handler/url-regex (val %)) (:block/properties page))]
- (if link
- (js/window.open link)
- (notification/show! "No link found on this page's properties." :warning)))
- nil)
- (state/close-modal!))
- (defn- search-on-chosen
- [repo search-q {:keys [type data alias]}]
- (search-handler/add-search-to-recent! repo search-q)
- (search-handler/clear-search!)
- (case type
- :graph-add-filter
- (state/add-graph-search-filter! search-q)
- :new-page
- (page-handler/create! search-q {:redirect? true})
- :new-class
- (let [search-q' (subs search-q 1)]
- (page-handler/create! search-q' {:class? true
- :redirect? false})
- (state/pub-event! [:class/configure (db/entity [:block/name (util/page-name-sanity-lc search-q')]) {}]))
- :new-whiteboard
- (whiteboard-handler/create-new-whiteboard-and-redirect! search-q)
- :page
- (let [data (or alias data)]
- (cond
- (model/whiteboard-page? data)
- (route-handler/redirect-to-whiteboard! data)
- :else
- (route-handler/redirect-to-page! data)))
- :file
- (route-handler/redirect! {:to :file
- :path-params {:path data}})
- :block
- (let [block-uuid (uuid (:block/uuid data))
- block-uuid (or
- (some-> (property-handler/get-property-block-created-block [:block/uuid block-uuid])
- db/entity
- :block/uuid)
- block-uuid)
- collapsed? (db/parents-collapsed? repo block-uuid)
- page (:block/page (db/entity [:block/uuid block-uuid]))
- page-name (:block/name page)]
- (if page
- (cond
- (model/whiteboard-page? page-name)
- (route-handler/redirect-to-whiteboard! page-name {:block-id block-uuid})
- collapsed?
- (route-handler/redirect-to-page! block-uuid)
- :else
- (route-handler/redirect-to-page! (:block/name page) {:anchor (str "ls-block-" (:block/uuid data))}))
- ;; search indice outdated
- (println "[Error] Block page missing: "
- {:block-id block-uuid
- :block (db/pull [:block/uuid block-uuid])})))
- :page-content
- (let [page-uuid (uuid (:block/uuid data))
- page (model/get-block-by-uuid page-uuid)
- page-name (:block/name page)]
- (if page
- (cond
- (model/whiteboard-page? page-name)
- (route-handler/redirect-to-whiteboard! page-name)
- :else
- (route-handler/redirect-to-page! page-name))
- ;; search indice outdated
- (println "[Error] page missing: "
- {:page-uuid page-uuid
- :page page})))
- nil)
- (state/close-modal!))
- (defn- search-on-shift-chosen
- [repo search-q {:keys [type data alias]}]
- (search-handler/add-search-to-recent! repo search-q)
- (case type
- :page
- (let [data (or alias data)
- page (when data (db/entity [:block/name (util/page-name-sanity-lc data)]))]
- (when page
- (state/sidebar-add-block!
- repo
- (:db/id page)
- :page)))
- :page-content
- (let [page-uuid (uuid (:block/uuid data))
- page (model/get-block-by-uuid page-uuid)]
- (if page
- (state/sidebar-add-block!
- repo
- (:db/id page)
- :page)
- ;; search indice outdated
- (println "[Error] page missing: "
- {:page-uuid page-uuid
- :page page})))
- :block
- (let [block-uuid (uuid (:block/uuid data))
- block (db/entity [:block/uuid block-uuid])]
- (state/sidebar-add-block!
- repo
- (:db/id block)
- :block))
- :new-page
- (page-handler/create! search-q)
- :new-class
- (page-handler/create! search-q {:class? true
- :redirect? false})
- :file
- (route-handler/redirect! {:to :file
- :path-params {:path data}})
- nil)
- (state/close-modal!))
- (defn- create-item-render
- [icon label name]
- (search-result-item
- {:name icon
- :class "highlight"
- :extension? true}
- [:div.text.font-bold label
- [:span.ml-2 name]]))
- (defn- search-item-render
- [search-q {:keys [type data alias]}]
- (let [search-mode (state/get-search-mode)
- data (if (string? data) (pdf-utils/fix-local-asset-pagename data) data)]
- [:div {:class "py-2"}
- (case type
- :graph-add-filter
- [:b search-q]
- :new-page
- (create-item-render "new-page" (t :new-page) (str "\"" (string/trim search-q) "\""))
- :new-class
- ;; TODO: Add icon for new-class
- (create-item-render "new-page" (t :new-class) (str "\"" (string/trim (subs search-q 1)) "\""))
- :new-whiteboard
- (create-item-render "new-whiteboard" (t :new-whiteboard) (str "\"" (string/trim search-q) "\""))
- :page
- [:span {:data-page-ref data}
- (when alias
- (let [target-original-name (model/get-page-original-name alias)]
- [:span.mr-2.text-sm.font-medium.mb-2 (str "Alias -> " target-original-name)]))
- (search-result-item {:name (if (model/whiteboard-page? data) "whiteboard" "page")
- :extension? true
- :title (t (if (model/whiteboard-page? data) :search-item/whiteboard :search-item/page))}
- (highlight/highlight-exact-query data search-q))]
- :file
- (search-result-item {:name "file"
- :title (t :search-item/file)}
- (highlight/highlight-exact-query data search-q))
- :block
- (let [{:block/keys [page uuid content]} data ;; content here is normalized
- page (util/get-page-original-name page)
- repo (state/sub :git/current-repo)
- format (db/get-page-format page)
- block (when-not (string/blank? uuid)
- (model/query-block-by-uuid uuid))
- content' (if block (:block/content block) content)]
- [:span {:data-block-ref uuid}
- (search-result-item {:name "block"
- :title (t :search-item/block)
- :extension? true}
- (cond
- (some? block)
- (block-search-result-item repo uuid format content' search-q search-mode)
- (not (string/blank? content'))
- content'
- :else
- (do (log/error "search result with non-existing uuid: " data)
- (t :search/cache-outdated))))])
- :page-content
- (let [{:block/keys [snippet uuid]} data ;; content here is normalized
- repo (state/sub :git/current-repo)
- page (when uuid (model/query-block-by-uuid uuid)) ;; it's actually a page
- format (db/get-page-format page)]
- (when page
- [:span {:data-block-ref uuid}
- (search-result-item {:name "page"
- :title (t :search-item/page)
- :extension? true}
- (if page
- (page-content-search-result-item repo uuid format snippet search-q search-mode)
- (do (log/error "search result with non-existing uuid: " data)
- (t :search/cache-outdated))))]))
- nil)]))
- (rum/defc search-auto-complete
- "has-more? - if the result is truncated
- all? - if true, in show-more mode"
- [{:keys [engine pages files pages-content blocks has-more?] :as result} search-q all?]
- (let [pages (when-not all? (map (fn [page]
- (let [alias (model/get-redirect-page-name page)]
- (cond->
- {:type :page
- :data page}
- (and alias
- (not= (util/page-name-sanity-lc page)
- (util/page-name-sanity-lc alias)))
- (assoc :alias alias))))
- (remove nil? pages)))
- files (when-not all? (map (fn [file] {:type :file :data file}) files))
- blocks (map (fn [block] {:type :block :data block}) blocks)
- pages-content (map (fn [pages-content] {:type :page-content :data pages-content}) pages-content)
- search-mode (state/sub :search/mode)
- tag-search? (= \# (first search-q))
- new-page (if (or
- (some? engine)
- (let [search-q' (util/safe-page-name-sanity-lc search-q)
- first-matched-item (util/safe-page-name-sanity-lc (:data (first pages)))]
- (and (seq pages)
- (or (= search-q' first-matched-item)
- (and tag-search? (= search-q' (str "#" first-matched-item))))))
- (nil? result)
- all?)
- []
- (if (state/enable-whiteboards?)
- [{:type (if tag-search? :new-class :new-page)} {:type :new-whiteboard}]
- [{:type :new-page}]))
- result (cond
- config/publishing?
- (concat pages files blocks) ;; Browser doesn't have page content FTS
- (= :whiteboard/link search-mode)
- (concat pages blocks pages-content)
- :else
- (concat new-page pages files blocks pages-content))
- result (if (= search-mode :graph)
- [{:type :graph-add-filter}]
- result)
- repo (state/get-current-repo)]
- [:div.results-inner
- (ui/auto-complete
- result
- {:class "search-results"
- :on-chosen #(search-on-chosen repo search-q %)
- :on-shift-chosen #(search-on-shift-chosen repo search-q %)
- :item-render #(search-item-render search-q %)
- :on-chosen-open-link #(search-on-chosen-open-link repo search-q %)})
- (when (and has-more? (not all?))
- [:div.px-2.py-4.search-more
- [:a.text-sm.font-medium {:href (rfe/href :search {:q search-q})
- :on-click (fn []
- (when-not (string/blank? search-q)
- (state/close-modal!)
- (search-handler/search (state/get-current-repo) search-q {:limit 1000
- :more? true})
- (search-handler/clear-search!)))}
- (t :more)]])]))
- (rum/defc recent-search-and-pages
- [in-page-search?]
- [:div.recent-search
- [:div.wrap.px-4.pb-2.text-sm.opacity-70.flex.flex-row.justify-between.align-items.mx-1.sm:mx-0
- [:div (t :search/recent)]
- [:div.hidden.md:flex
- (ui/with-shortcut :go/search-in-page "bottom"
- [:div.flex-row.flex.align-items
- [:div.mr-3.flex (t :search/blocks-in-page)]
- [:div.flex.items-center
- (ui/toggle in-page-search?
- (fn [_value]
- (state/set-search-mode! (if in-page-search? :global :page)))
- true)]
- (ui/tippy {:html [:div
- ;; TODO: fetch from config
- (t :search/command-palette-tip-1) [:code (util/->platform-shortcut "Ctrl + Shift + p")] (t :search/command-palette-tip-2)]
- :interactive true
- :arrow true
- :theme "monospace"}
- [:a.flex.fade-link.items-center
- {:style {:margin-left 12}
- :on-click #(state/pub-event! [:modal/command-palette])}
- (ui/icon "command" {:style {:font-size 20}})])])]]
- (let [recent-search (mapv (fn [q] {:type :search :data q})
- (if (config/db-based-graph? (state/get-current-repo))
- (state/get-recent-search)
- (db/get-key-value :recent/search)))
- pages (->> (recent-handler/get-recent-pages)
- (mapv (fn [page] {:type :page :data page})))
- result (concat (take 5 recent-search) pages)]
- (ui/auto-complete
- result
- {:on-chosen (fn [{:keys [type data]}]
- (case type
- :page
- (do (route-handler/redirect-to-page! data)
- (state/close-modal!))
- :search
- (let [q data]
- (state/set-q! q)
- (let [search-mode (state/get-search-mode)
- opts (if (= :page search-mode)
- (let [current-page (or (state/get-current-page)
- (date/today))]
- {:page-db-id (:db/id (db/entity [:block/name (util/page-name-sanity-lc current-page)]))})
- {})]
- (if (= :page search-mode)
- (search-handler/search (state/get-current-repo) q opts)
- (search-handler/search (state/get-current-repo) q))))
- nil))
- :on-shift-chosen (fn [{:keys [type data]}]
- (case type
- :page
- (let [page data]
- (when (string? page)
- (when-let [page (db/pull [:block/name (util/page-name-sanity-lc page)])]
- (state/sidebar-add-block!
- (state/get-current-repo)
- (:db/id page)
- :page))
- (state/close-modal!)))
- nil))
- :item-render (fn [{:keys [type data]}]
- (case type
- :search [:div.flex-row.flex.search-item.font-medium
- svg/search
- [:span.ml-2 data]]
- :page (when-let [original-name (model/get-page-original-name data)] ;; might be block reference
- (search-result-item {:name "page"
- :extension? true}
- original-name))
- nil))}))])
- (defn default-placeholder
- [search-mode]
- (cond
- config/publishing?
- (t :search/publishing)
- (= search-mode :whiteboard/link)
- (t :whiteboard/link-whiteboard-or-block)
- :else
- (t :search)))
- (rum/defcs search-modal < rum/reactive
- shortcut/disable-all-shortcuts
- (mixins/event-mixin
- (fn [state]
- (mixins/hide-when-esc-or-outside
- state
- :on-hide (fn []
- (search-handler/clear-search!)))))
- (rum/local nil ::active-engine-tab)
- [state]
- (let [search-result (state/sub :search/result)
- search-q (state/sub :search/q)
- search-mode (state/sub :search/mode)
- engines (state/sub :search/engines)
- *active-engine-tab (::active-engine-tab state)
- timeout 300
- in-page-search? (= search-mode :page)]
- [:div.cp__palette.cp__palette-main
- [:div.ls-search.p-2.md:p-0
- [:div.input-wrap
- [:input.cp__palette-input.w-full.h-full
- {:type "text"
- :auto-focus true
- :placeholder (case search-mode
- :graph
- (t :graph-search)
- :page
- (t :page-search)
- (default-placeholder search-mode))
- :auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here
- :value search-q
- :on-key-down (fn [^js e]
- (when (= 27 (.-keyCode e))
- (when-not (string/blank? search-q)
- (util/stop e)
- (search-handler/clear-search!))))
- :on-change (fn [^js e]
- (when @search-timeout
- (js/clearTimeout @search-timeout))
- (let [value (util/evalue e)
- is-composing? (util/onchange-event-is-composing? e)] ;; #3199
- (if (and (string/blank? value) (not is-composing?))
- (search-handler/clear-search! false)
- (let [search-mode (state/get-search-mode)
- opts (if (= :page search-mode)
- (when-let [current-page (or (state/get-current-page)
- (date/today))]
- {:page-db-id (:db/id (db/entity [:block/name (util/page-name-sanity-lc current-page)]))})
- {})]
- (state/set-q! value)
- (reset! search-timeout
- (js/setTimeout
- (fn []
- (if (= :page search-mode)
- (search-handler/search (state/get-current-repo) value opts)
- (search-handler/search (state/get-current-repo) value)))
- timeout))))))}]]
- [:div.search-results-wrap
- ;; list registered search engines
- (when (seq engines)
- [:ul.search-results-engines-tabs
- [:li
- {:class (when-not @*active-engine-tab "is-active")}
- (ui/button
- [:span.flex.items-center
- (svg/logo 14) [:span.pl-2 "Default"]]
- :background "orange"
- :on-click #(reset! *active-engine-tab nil))]
- (for [[k v] engines]
- [:li
- {:key k
- :class (if (= k @*active-engine-tab) "is-active" "")}
- (ui/button [:span.flex.items-center
- [:span.pr-2 (ui/icon "puzzle")]
- (:name v)
- (when-let [result (and v (:result v))]
- (str " (" (apply + (map count ((juxt :blocks :pages :files) result))) ")"))]
- :on-click #(reset! *active-engine-tab k))])])
- (if-not (nil? @*active-engine-tab)
- (let [active-engine-result (get-in engines [@*active-engine-tab :result])]
- (search-auto-complete
- (merge active-engine-result {:engine @*active-engine-tab}) search-q false))
- (if (seq search-result)
- (search-auto-complete search-result search-q false)
- (recent-search-and-pages in-page-search?)))]]]))
- (rum/defc more < rum/reactive
- [route]
- (let [search-q (get-in route [:path-params :q])
- search-result (state/sub :search/more-result)]
- [:div#search.flex-1.flex
- [:div.inner
- [:h1.title (t :search/result-for) [:i search-q]]
- [:p.font-medium.tx-sm (str (count (:blocks search-result)) " " (t :search/items))]
- [:div#search-wrapper.relative.w-full.text-gray-400.focus-within:text-gray-600
- (when-not (string/blank? search-q)
- (search-auto-complete search-result search-q true))]]]))
|