cmdk initial implementation

This commit is contained in:
Ben Yorke
2023-08-02 23:07:05 +03:00
parent 6f984d45ef
commit 8bff0f2139
14 changed files with 777 additions and 119 deletions

View File

@@ -541,15 +541,16 @@
(let [*mouse-down? (::mouse-down? state)
tag? (:tag? config)
config (assoc config :whiteboard-page? whiteboard-page?)
untitled? (model/untitled-page? page-name)
gradient-styles (state/sub-color-gradient-text-styles :09)]
untitled? (model/untitled-page? page-name)]
; gradient-styles (state/sub-color-gradient-text-styles :09)]
[:a
{:tabIndex "0"
:class (cond-> (if tag? "tag" "page-ref")
(:property? config)
(str " page-property-key block-property")
untitled? (str " opacity-50"))
:style gradient-styles
:style {:color "var(--lx-accent-10)"}
:data-ref page-name
:draggable true
:on-drag-start (fn [e] (editor-handler/block->data-transfer! page-name-in-block e))
@@ -758,10 +759,7 @@
(excalidraw s block-uuid)]
[:span.page-reference
{:data-ref s
:style {:background-image (colors/linear-gradient :grass "09" 5)
:background-clip "text"
"-webkit-background-clip" "text"
:color "transparent"}}
:style {:color "var(--lx-accent-09)"}}
(when (and (or show-brackets? nested-link?)
(not html-export?)
(not contents-page?))
@@ -2286,12 +2284,12 @@
:data-type (name block-type)
:style {:width "100%" :pointer-events (when stop-events? "none")}}
(not (string/blank? (:hl-color properties)))
(assoc :data-hl-color (:hl-color properties))
(not (string/blank? (:hl-color properties)))
(assoc :data-hl-color (:hl-color properties))
(not block-ref?)
(assoc mouse-down-key (fn [e]
(block-content-on-mouse-down e block block-id content edit-input-id))))]
(not block-ref?)
(assoc mouse-down-key (fn [e]
(block-content-on-mouse-down e block block-id content edit-input-id))))]
[:div.block-content.inline
(cond-> {:id (str "block-content-" uuid)
:class (when selected? "select-none")
@@ -2809,20 +2807,20 @@
:blockid (str uuid)
:haschild (str (boolean has-child?))}
level
(assoc :level level)
level
(assoc :level level)
(not slide?)
(merge attrs)
(not slide?)
(merge attrs)
(or reference? embed?)
(assoc :data-transclude true)
(or reference? embed?)
(assoc :data-transclude true)
embed?
(assoc :data-embed true)
embed?
(assoc :data-embed true)
custom-query?
(assoc :data-query true))
custom-query?
(assoc :data-query true))
(when (and ref? breadcrumb-show?)
(breadcrumb config repo uuid {:show-page? false

View File

@@ -0,0 +1,346 @@
(ns frontend.components.cmdk
(:require
[clojure.string :as string]
[frontend.components.block :as block]
[frontend.components.command-palette :as cp]
[frontend.components.page :as page]
[frontend.context.i18n :refer [t]]
[frontend.db :as db]
[frontend.db.model :as model]
[frontend.handler.command-palette :as cp-handler]
[frontend.handler.editor :as editor-handler]
[frontend.handler.route :as route-handler]
[frontend.handler.search :as search-handler]
[frontend.modules.shortcut.core :as shortcut]
[frontend.modules.shortcut.data-helper :as shortcut-helper]
[frontend.search :as search]
[frontend.state :as state]
[frontend.ui :as ui]
[frontend.util :as util]
[goog.functions :as gfun]
[logseq.shui.context :refer [make-context]]
[logseq.shui.core :as shui]
[promesa.core :as p]
[rum.core :as rum]))
;; When CMDK opens, we have some default search actions we make avaialbe for quick access
(def default-search-actions
[{:text "Search only pages" :info "Add filter to search"}
{:text "Search only blocks" :info "Add filter to search"}
{:text "Create block" :info "Add a block to today's journal page" :icon "block" :icon-theme :color}
{:text "Generate short answer" :info "Ask a language model" :icon "question-mark" :icon-theme :gradient}
{:text "Open settings" :icon "settings" :icon-theme :gray}])
;; The results are separated into groups, and loaded/fetched/queried separately
(def default-results
{:search-actions {:status :success :show-more false :items default-search-actions}
:commands {:status :success :show-more false :items nil}
:history {:status :success :show-more false :items nil}
:current-page {:status :success :show-more false :items nil}
:pages {:status :success :show-more false :items nil}
:blocks {:status :success :show-more false :items nil}
:files {:status :success :show-more false :items nil}})
;; Each result gorup has it's own load-results function
(defmulti load-results (fn [group state] group))
;; The search-actions are only loaded when there is no query present. It's like a quick access to filters
(defmethod load-results :search-actions [group state]
(let [!input (::input state)
!results (::results state)]
(if (empty? @!input)
(swap! !results assoc group {:status :success :items default-search-actions})
(swap! !results assoc group {:status :success :items nil}))))
;; The commands search uses the command-palette hander
(defmethod load-results :commands [group state]
(let [!input (::input state)
!results (::results state)]
(swap! !results assoc-in [group :status] :loading)
(->> (vals (cp-handler/get-commands-unique))
(filter #(string/includes? (string/lower-case (pr-str %)) (string/lower-case @!input)))
(map #(hash-map :icon "command"
:icon-theme :gray
:text (cp/translate t %)
:value-label (pr-str (:id %))
; :info (pr-str (:id %))
; :info (:desc %)
:shortcut (:shortcut %)
:source-command %))
(hash-map :status :success :items)
(swap! !results assoc group))
(js/console.log "commands" (clj->js (get-in @!results [:commands :items])))))
;; The pages search action uses an existing handler
(defmethod load-results :pages [group state]
(let [!input (::input state)
!results (::results state)]
(swap! !results assoc-in [group :status] :loading)
(p/let [pages (search/page-search @!input)
items (map #(hash-map :icon "page"
:icon-theme :gray
:text %
:source-page %) pages)]
(js/console.log "pages" (pr-str pages) (clj->js items))
(swap! !results assoc group {:status :success :items items}))))
;; The blocks search action uses an existing handler
(defmethod load-results :blocks [group state]
(let [!input (::input state)
!results (::results state)
repo (state/get-current-repo)
opts {:limit 100}]
(swap! !results assoc-in [group :status] :loading)
(p/let [blocks (search/block-search repo @!input opts)
items (map #(hash-map :icon "block"
:icon-theme :gray
:text (:block/content %)
:header (some-> % :block/page db/entity :block/name)
:source-block %) blocks)]
; (js/console.log "blocks" (clj->js items) (map (comp pr-str :block/page) blocks))
; (js/console.log "blocks" (clj->js items)
; (pr-str (map (comp pr-str :block/page) blocks))
; (pr-str (map (comp :block/name :block/page) blocks))
; (pr-str (map (comp :block/name db/entity :block/page) blocks)))
(swap! !results assoc group {:status :success :items items}))))
;; The default load-results function triggers all the other load-results function
(defmethod load-results :default [_ state]
(js/console.log "load-results/default" @(::input state))
(load-results :search-actions state)
(load-results :commands state)
(load-results :blocks state)
(load-results :pages state))
; (def search [query]
; (load-results :search-actions state))
; (let [repo (state/get-current-repo)
; limit 5
; current-page-db-id nil
; opts {:limit limit}]
; (p/let [blocks (search/block-search repo q opts)
; pages (search/page-search q)
; pages-content (when current-page-db-id (search/page-content-search repo q opts))
; files (search/file-search q)
; commands])))
(defmulti handle-action (fn [action _state _item _event] action))
(defmethod handle-action :cancel [_ state item event]
(js/console.log :handle-action/cancel)
(state/close-modal!))
(defmethod handle-action :copy-page-ref [_ state item event]
(when-let [page-name (:source-page item)]
(util/copy-to-clipboard! page-name)
(state/close-modal!)))
(defmethod handle-action :copy-block-ref [_ state item event]
(when-let [block-uuid (some-> item :source-block :block/uuid uuid)]
(editor-handler/copy-block-ref! block-uuid)
(state/close-modal!)))
(defmethod handle-action :open-page [_ state item event]
(when-let [page-name (:source-page item)]
(route-handler/redirect-to-page! page-name)
(state/close-modal!)))
(defmethod handle-action :open-block [_ state item event]
(let [get-block-page (partial model/get-block-page (state/get-current-repo))]
(when-let [page (some-> item :source-block :block/uuid uuid get-block-page :block/name model/get-redirect-page-name)]
(route-handler/redirect-to-page! page)
(state/close-modal!))))
(defmethod handle-action :open-page-right [_ state item event]
(when-let [page-uuid (some-> item :source-page model/get-page :block/uuid uuid)]
(js/console.log "oepn-page-right" page-uuid)
(editor-handler/open-block-in-sidebar! page-uuid)))
(defmethod handle-action :open-block-right [_ state item event]
(when-let [block-uuid (some-> item :source-block :block/uuid uuid)]
(js/console.log "oepn-block-right" block-uuid)
(editor-handler/open-block-in-sidebar! block-uuid)))
(defmethod handle-action :trigger [_ state item event])
(defmethod handle-action :return [_ state item event])
(rum/defc result-group < rum/reactive
[state title group visible-items highlighted-result]
(let [{:keys [show-more items]} (some-> state ::results deref group)]
[:div {:class ""}
[:div {:class "text-xs py-1.5 px-6 flex justify-between items-center gap-2"
:style {:color "var(--lx-gray-11)"
:background "var(--lx-gray-02)"}}
[:div {:class "font-bold"
:style {:color "var(--lx-gray-11)"}} title]
[:div {:class "bg-white/20 px-1.5 py-px text-white rounded-full"
:style {:font-size "0.6rem"}}
(if (<= 100 (count items))
(str "99+")
(count items))]
[:div {:class "flex-1"}]
(if show-more
[:div {:on-click #(swap! (::results state) update-in [group :show-more] not)} "Show less"]
[:div {:on-click #(swap! (::results state) update-in [group :show-more] not)} "Show more"])]
[:div {:class ""}
(for [result visible-items
:let [highlighted? (= result highlighted-result)]]
(shui/list-item (assoc result
:highlighted highlighted?
:on-highlight (fn [ref]
(.. ref -current (scrollIntoView #js {:block "center"
:inline "nearest"
:behavior "smooth"}))
(case group
:search-actions (reset! (::actions state) [:cancel :return])
:commands (reset! (::actions state) [:cancel :trigger])
:pages (reset! (::actions state) [:cancel :copy-page-ref :open-page-right :open-page])
:blocks (reset! (::actions state) [:cancel :copy-block-ref :open-block-right :open-block])
nil)))))]]))
(defonce keydown-handler
(fn [state e]
(when (#{"ArrowDown" "ArrowUp"} (.-key e))
(.preventDefault e))
(case (.-key e)
; "Escape" (rum/dispatch! :close)
"ArrowDown" (swap! (::highlight-index state) inc)
"ArrowUp" (swap! (::highlight-index state) dec)
; "j" (when (.-metaKey e)
; (if (.-shiftKey e)
; (swap! state update :current-engine prev-engine)
; (swap! state update :current-engine next-engine)))
; "ArrowUp" (rum/dispatch! :highlight-prev)
; "Enter" (rum/dispatch! :select)
(println (.-key e)))))
(defn handle-input-change [state e]
(let [input (.. e -target -value)
!input (::input state)
!load-results-throttled (::load-results-throttled state)]
;; update the input value in the UI
(reset! !input input)
;; ensure that there is a throttled version of the load-results function
(when-not @!load-results-throttled
(reset! !load-results-throttled (gfun/throttle load-results 1000)))
;; retreive the laod-results function and update all the results
(when-let [load-results-throttled @!load-results-throttled]
(load-results-throttled :all state))))
(rum/defc page-preview [state highlighted]
(let [page-name (:source-page highlighted)]
(page/page {:page-name (model/get-redirect-page-name page-name) :whiteboard? true})))
(rum/defc block-preview [state highlighted]
(let [block (:source-block highlighted)
block-uuid-str (str (:block/uuid block))]
; ((state/get-component :block/single-block) (uuid (:block/uuid block)))))
; ((state/get-component :block/container) block)
; ((state/get-component :block/embed) (uuid (:block/uuid block)))))
; (block/block-container {} block)))
(page/page {:parameters {:path {:name block-uuid-str}}
:sidebar? true
:repo (state/get-current-repo)})))
(rum/defcs cmdk <
shortcut/disable-all-shortcuts
(rum/local "" ::input)
(rum/local 0 ::highlight-index)
(rum/local nil ::keydown-handler)
(rum/local default-results ::results)
(rum/local nil ::load-results-throttled)
(rum/local [:cancel :return] ::actions)
{:did-mount (fn [state]
(let [next-keydown-handler (partial keydown-handler state)]
(when-let [prev-keydown-handler @(::keydown-handler state)]
(js/window.removeEventListener "keydown" prev-keydown-handler))
(js/window.addEventListener "keydown" next-keydown-handler)
(reset! (::keydown-handler state) next-keydown-handler))
state)
:will-unmount (fn [state]
(when-let [current-keydown-handler (some-> state ::keydown-handler deref)]
(js/window.removeEventListener "keydown" current-keydown-handler))
(reset! (::keydown-handler state) nil)
state)}
[state {:keys []}]
(let [input @(::input state)
actions @(::actions state)
highlight-index @(::highlight-index state)
results @(::results state)
visible-items-for-group (fn [group]
(let [{:keys [items show-more]} (get results group)]
(if show-more items (take 5 items))))
results-ordered [["Search actions" :search-actions (visible-items-for-group :search-actions)]
["Commands" :commands (visible-items-for-group :commands)]
["Pages" :pages (visible-items-for-group :pages)]
["Blocks" :blocks (visible-items-for-group :blocks)]]
results (mapcat last results-ordered)
result-count (count results)
highlighted-result-index (cond
(zero? result-count) nil
(<= 0 (mod highlight-index result-count)) (mod highlight-index result-count)
:else (- result-count (mod highlight-index result-count)))
highlighted-result (when highlighted-result-index
(nth results highlighted-result-index nil))
preview? (or (:source-page highlighted-result) (:source-block highlighted-result))]
[:div.cp__cmdk {:class "-m-8 max-w-[90dvw] max-h-[90dvh] w-[60rem] h-[30.7rem] "}
[:div {:class ""
:style {:background "var(--lx-gray-02)"
:border-bottom "1px solid var(--lx-gray-07)"}}
[:input {:class "text-xl bg-transparent border-none w-full outline-none px-4 py-3"
:placeholder "What are you looking for?"
:ref #(when % (.focus %))
:on-change (partial handle-input-change state)
:value input}]]
[:div {:class (str "grid" (if preview? " grid-cols-2" " grid-cols-1"))}
[:div {:class "pt-1 overflow-y-auto h-96"
:style {:background "var(--lx-gray-02)"}}
(for [[group-name group-key group-items] results-ordered
:when (not-empty group-items)]
(result-group state group-name group-key group-items highlighted-result))]
(when preview?
[:div {:class "h-96 overflow-y-auto"}
(cond
(:source-page highlighted-result)
(page-preview state highlighted-result)
(:source-block highlighted-result)
(block-preview state highlighted-result))])]
[:div {:class "flex justify-between w-full px-4"
:style {:background "var(--lx-gray-03)"
:border-top "1px solid var(--lx-gray-07)"}}
[:div {:class "flex items-stretch gap-2"}
(for [[tab-name tab-icon] [["Search" "search"]
["Capture" "square-plus"]]
:let [active? (= tab-name "Search")]]
[:div {:class "flex items-center px-1.5 gap-1 relative"}
(when active?
[:div {:class "absolute inset-x-0 top-0 h-0.5 bg-gray-500"}])
(when active?
(shui/icon tab-icon {:size "16"}))
[:div {:class ""} tab-name]])]
[:div {:class "flex items-center py-3 gap-4"}
(for [action actions
:let [on-click (partial handle-action action state highlighted-result)]]
(case action
:cancel (shui/button {:text "Cancel" :theme :gray :on-click on-click :shortcut ["esc"]})
:copy-page-ref (shui/button {:text "Copy" :theme :gray :on-click on-click :shortcut ["cmd" "c"]})
:copy-block-ref (shui/button {:text "Copy" :theme :gray :on-click on-click :shortcut ["cmd" "c"]})
:open-page-right (shui/button {:text "Open in sidebar" :theme :gray :on-click on-click :shortcut ["shift" "return"]})
:open-page (shui/button {:text "Open" :theme :color :on-click on-click :shortcut ["return"]})
:open-block-right (shui/button {:text "Open in sidebar" :theme :gray :on-click on-click :shortcut ["shift" "return"]})
:open-block (shui/button {:text "Open page" :theme :color :on-click on-click :shortcut ["return"]})
:trigger (shui/button {:text "Trigger" :theme :color :on-click on-click :shortcut ["return"]})
:return (shui/button {:text "Return" :theme :color :on-click on-click :shortcut ["return"]})))]]]))
; (shui/button {:text "AI" :theme :gradient} (make-context {}))]]]))
(->> (cp-handler/get-commands-unique)
vals
(map (juxt :shortcut :id)))

View File

@@ -319,21 +319,21 @@
[:div.cp__right-sidebar-settings.hide-scrollbar.gap-1 {:key "right-sidebar-settings"}
[:div.text-sm
[:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
(state/sidebar-add-block! repo "contents" :contents))}
(state/sidebar-add-block! repo "contents" :contents))}
(t :right-side-bar/contents)]]
[:div.text-sm
[:button.button.cp__right-sidebar-settings-btn {:on-click (fn []
(when-let [page (get-current-page)]
(state/sidebar-add-block!
repo
page
:page-graph)))}
(when-let [page (get-current-page)]
(state/sidebar-add-block!
repo
page
:page-graph)))}
(t :right-side-bar/page-graph)]]
[:div.text-sm
[:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
(state/sidebar-add-block! repo "help" :help))}
(state/sidebar-add-block! repo "help" :help))}
(t :right-side-bar/help)]]
(when config/dev? [:div.text-sm

View File

@@ -190,9 +190,9 @@
'[:find ?path
;; ?modified-at
:where
[?file :file/path ?path]
[?file :file/path ?path]]
;; [?file :file/last-modified-at ?modified-at]
]
db)
(seq)
;; (sort-by last)
@@ -919,6 +919,7 @@ independent of format as format specific heading characters are stripped"
(defn get-block-page
[repo block-uuid]
(assert (uuid? block-uuid) "get-block-page requires block-uuid to be of type uuid")
(when-let [block (db-utils/entity repo [:block/uuid block-uuid])]
(db-utils/entity repo (:db/id (:block/page block)))))

View File

@@ -190,6 +190,8 @@
(state/set-component! :block/linked-references reference/block-linked-references)
(state/set-component! :whiteboard/tldraw-preview whiteboard/tldraw-preview)
(state/set-component! :block/single-block block/single-block-cp)
(state/set-component! :block/container block/block-container)
(state/set-component! :block/embed block/block-embed)
(state/set-component! :editor/box editor/box)
(command-palette/register-global-shortcut-commands))

View File

@@ -193,8 +193,16 @@
(state/set-edit-content! edit-id new-value)
(cursor/move-cursor-to input (+ cur-pos forward-pos))))))
(-> (random-uuid) str)
(defn open-block-in-sidebar!
[block-id]
; (assert (uuid? block-id) "frontend.handler.editor/open-block-in-sidebar! expects block-id to be of type uuid")
(js/console.log "db-entity/block" block-id)
(js/console.log "db-entity/entity" (db/entity [:block/uuid block-id]))
; (js/console.log "db-entity/types" (str block-id) (uuid block-id))
; (js/console.log "db-entity/string" (db/entity [:block/uuid (str block-id)]))
; (js/console.log "db-entity/uuid" (db/entity [:block/uuid (uuid block-id)]))
(when block-id
(when-let [block (db/entity [:block/uuid block-id])]
(let [page? (nil? (:block/page block))]
@@ -831,9 +839,9 @@
concat-prev-block? (boolean (and prev-block new-content))
transact-opts (cond->
{:outliner-op :delete-blocks}
concat-prev-block?
(assoc :concat-data
{:last-edit-block (:block/uuid block)}))]
concat-prev-block?
(assoc :concat-data
{:last-edit-block (:block/uuid block)}))]
(outliner-tx/transact! transact-opts
(if concat-prev-block?
(let [prev-block' (if (seq (:block/_refs block-e))
@@ -1267,7 +1275,7 @@
(defn save-block!
([repo block-or-uuid content]
(save-block! repo block-or-uuid content {}))
(save-block! repo block-or-uuid content {}))
([repo block-or-uuid content {:keys [properties] :as opts}]
(let [block (if (or (uuid? block-or-uuid)
(string? block-or-uuid))

View File

@@ -12,12 +12,15 @@
[clojure.string :as string]
[datascript.core :as d]
[frontend.commands :as commands]
[frontend.components.block :as block]
[frontend.components.cmdk :as cmdk]
[frontend.components.command-palette :as command-palette]
[frontend.components.conversion :as conversion-component]
[frontend.components.diff :as diff]
[frontend.components.encryption :as encryption]
[frontend.components.file-sync :as file-sync]
[frontend.components.git :as git-component]
[frontend.components.page :as page]
[frontend.components.plugins :as plugin]
[frontend.components.search :as component-search]
[frontend.components.shell :as shell]
@@ -69,7 +72,6 @@
[goog.dom :as gdom]
[logseq.db.schema :as db-schema]
[logseq.graph-parser.config :as gp-config]
[logseq.shui.core :refer [cmdk]]
[promesa.core :as p]
[rum.core :as rum]))
@@ -412,8 +414,10 @@
:label "ls-modal-search"}))
(defmethod handle :go/cmdk [_]
(when-not (= cmdk (:modal/panel-content @state/state))
(state/set-modal! cmdk
(when-not (= cmdk/cmdk (:modal/panel-content @state/state))
(state/set-modal! ; (partial cmdk {} (make-context {:blocks-container block/blocks-container :page-cp block/page-cp :page page/page}))
; cmdk
cmdk/cmdk
{:fullscreen? false
:close-btn? false
:label "ls-modal-cmdk"

View File

@@ -3,7 +3,8 @@
(:require
[frontend.date :refer [int->local-time-2]]
[frontend.state :as state]
[logseq.shui.context :refer [make-context]]))
[logseq.shui.context :refer [make-context]]
[frontend.handler.search :as search-handler]))
(def default-versions {:logseq.table.version 1})
@@ -23,4 +24,5 @@
:app-config (state/get-config)
:inline inline
:int->local-time-2 int->local-time-2
:state state/state}))
:state state/state
:search search-handler/search}))