builder.cljs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. (ns frontend.components.query.builder
  2. "DSL query builder."
  3. (:require [frontend.date :as date]
  4. [frontend.ui :as ui]
  5. [frontend.db :as db]
  6. [frontend.db.async :as db-async]
  7. [frontend.db.model :as db-model]
  8. [frontend.db.query-dsl :as query-dsl]
  9. [frontend.handler.editor :as editor-handler]
  10. [frontend.handler.query.builder :as query-builder]
  11. [frontend.components.select :as component-select]
  12. [frontend.state :as state]
  13. [frontend.util :as util]
  14. [logseq.shui.ui :as shui]
  15. [frontend.search :as search]
  16. [frontend.mixins :as mixins]
  17. [logseq.graph-parser.db :as gp-db]
  18. [rum.core :as rum]
  19. [clojure.string :as string]
  20. [logseq.common.util :as common-util]
  21. [logseq.common.util.page-ref :as page-ref]
  22. [promesa.core :as p]))
  23. (rum/defc page-block-selector
  24. [*find]
  25. [:div.filter-item {:on-pointer-down (fn [e] (util/stop-propagation e))}
  26. (ui/select [{:label "Blocks"
  27. :value "block"
  28. :selected (not= @*find :page)}
  29. {:label "Pages"
  30. :value "page"
  31. :selected (= @*find :page)}]
  32. (fn [e v]
  33. ;; Prevent opening the current block's editor
  34. (util/stop e)
  35. (reset! *find (keyword v))))])
  36. (defn- select
  37. ([items on-chosen]
  38. (select items on-chosen {}))
  39. ([items on-chosen options]
  40. (component-select/select (merge
  41. {:items (map #(hash-map :value %) items)
  42. :on-chosen on-chosen}
  43. options))))
  44. (defn append-tree!
  45. [*tree {:keys [toggle-fn toggle?]
  46. :or {toggle? true}} loc x]
  47. (swap! *tree #(query-builder/append-element % loc x))
  48. (when toggle? (toggle-fn)))
  49. (rum/defcs search < (rum/local nil ::input-value)
  50. (mixins/event-mixin
  51. (fn [state]
  52. (mixins/on-key-down
  53. state
  54. {;; enter
  55. 13 (fn [state e]
  56. (let [input-value (get state ::input-value)]
  57. (when-not (string/blank? @input-value)
  58. (util/stop e)
  59. (let [on-submit (first (:rum/args state))]
  60. (on-submit @input-value))
  61. (reset! input-value nil))))
  62. ;; escape
  63. 27 (fn [_state _e]
  64. (let [[_on-submit on-cancel] (:rum/args state)]
  65. (on-cancel)))})))
  66. [state _on-submit _on-cancel]
  67. (let [*input-value (::input-value state)]
  68. [:input#query-builder-search.form-input.block.sm:text-sm.sm:leading-5
  69. {:auto-focus true
  70. :placeholder "Full text search"
  71. :aria-label "Full text search"
  72. :on-change #(reset! *input-value (util/evalue %))}]))
  73. (defonce *between-dates (atom {}))
  74. (rum/defcs datepicker < rum/reactive
  75. (rum/local nil ::input-value)
  76. {:will-unmount (fn [state]
  77. (swap! *between-dates dissoc (first (:rum/args state)))
  78. state)}
  79. [state id placeholder {:keys [auto-focus]}]
  80. (let [*input-value (::input-value state)]
  81. [:div.ml-4
  82. [:input.query-builder-datepicker.form-input.block.sm:text-sm.sm:leading-5
  83. {:auto-focus (or auto-focus false)
  84. :placeholder placeholder
  85. :aria-label placeholder
  86. :value (some-> @*input-value (first))
  87. :on-focus (fn [^js e]
  88. (js/setTimeout
  89. #(shui/popup-show! (.-target e)
  90. (let [select-handle! (fn [^js d]
  91. (let [gd (date/js-date->goog-date d)
  92. journal-date (date/js-date->journal-title gd)]
  93. (reset! *input-value [journal-date d])
  94. (swap! *between-dates assoc id journal-date))
  95. (shui/popup-hide!))]
  96. (shui/calendar
  97. {:mode "single"
  98. :initial-focus true
  99. :selected (some-> @*input-value (second))
  100. :on-select select-handle!
  101. :on-day-key-down (fn [^js d _ ^js e]
  102. (when (= "Enter" (.-key e))
  103. (select-handle! d)
  104. (util/stop e)))}))
  105. {:id :query-datepicker
  106. :align :start}) 16))}]]))
  107. (rum/defcs between <
  108. (rum/local nil ::start)
  109. (rum/local nil ::end)
  110. [state {:keys [tree loc] :as opts}]
  111. [:div.between-date.p-4 {:on-pointer-down (fn [e] (util/stop-propagation e))}
  112. [:div.flex.flex-row
  113. [:div.font-medium.mt-2 "Between: "]
  114. (datepicker :start "Start date" (merge opts {:auto-focus true}))
  115. (datepicker :end "End date" opts)]
  116. [:p.pt-2
  117. (ui/button "Submit"
  118. :on-click (fn []
  119. (let [{:keys [start end]} @*between-dates]
  120. (when (and start end)
  121. (let [clause [:between [:page-ref start] [:page-ref end]]]
  122. (append-tree! tree opts loc clause)
  123. (reset! *between-dates {}))))))]])
  124. (rum/defc property-select
  125. [*mode *property]
  126. (let [[properties set-properties!] (rum/use-state nil)]
  127. (rum/use-effect!
  128. (fn []
  129. (p/let [properties (search/get-all-properties)]
  130. (set-properties! properties)))
  131. [])
  132. (select properties
  133. (fn [{:keys [value]}]
  134. (reset! *mode "property-value")
  135. (reset! *property (keyword value))))))
  136. (rum/defc property-value-select
  137. [repo *property *find *tree opts loc]
  138. (let [[values set-values!] (rum/use-state nil)]
  139. (rum/use-effect!
  140. (fn []
  141. (p/let [result (db-async/<get-property-values repo @*property)]
  142. (set-values! result)))
  143. [@*property])
  144. (let [values (cons "Select all" values)]
  145. (select values
  146. (fn [{:keys [value]}]
  147. (let [x (if (= value "Select all")
  148. [(if (= @*find :page) :page-property :property) @*property]
  149. [(if (= @*find :page) :page-property :property) @*property value])]
  150. (reset! *property nil)
  151. (append-tree! *tree opts loc x)))))))
  152. (defn- query-filter-picker
  153. [state *find *tree loc clause opts]
  154. (let [*mode (::mode state)
  155. *property (::property state)
  156. repo (state/get-current-repo)]
  157. [:div
  158. (case @*mode
  159. "namespace"
  160. (let [items (sort (db-model/get-all-namespace-parents repo))]
  161. (select items
  162. (fn [{:keys [value]}]
  163. (append-tree! *tree opts loc [:namespace value]))))
  164. "tags"
  165. (let [items (->> (db-model/get-all-tagged-pages repo)
  166. (map second)
  167. sort)]
  168. (select items
  169. (fn [{:keys [value]}]
  170. (append-tree! *tree opts loc [:page-tags value]))))
  171. "property"
  172. (property-select *mode *property)
  173. "property-value"
  174. (property-value-select repo *property *find *tree opts loc)
  175. "sample"
  176. (select (range 1 101)
  177. (fn [{:keys [value]}]
  178. (append-tree! *tree opts loc [:sample (util/safe-parse-int value)])))
  179. "task"
  180. (select gp-db/built-in-markers
  181. (fn [value]
  182. (when (seq value)
  183. (append-tree! *tree opts loc (vec (cons :task value)))))
  184. {:multiple-choices? true
  185. ;; Need the existing choices later to improve the UX
  186. :selected-choices #{}
  187. :extract-chosen-fn :value
  188. :prompt-key :select/default-select-multiple
  189. :close-modal? false
  190. :on-apply (:toggle-fn opts)})
  191. "priority"
  192. (select gp-db/built-in-priorities
  193. (fn [value]
  194. (when (seq value)
  195. (append-tree! *tree opts loc (vec (cons :priority value)))))
  196. {:multiple-choices? true
  197. :selected-choices #{}
  198. :extract-chosen-fn :value
  199. :prompt-key :select/default-select-multiple
  200. :close-modal? false
  201. :on-apply (:toggle-fn opts)})
  202. "page"
  203. (let [pages (sort (db-model/get-all-page-original-names repo))]
  204. (select pages
  205. (fn [{:keys [value]}]
  206. (append-tree! *tree opts loc [:page value]))))
  207. "page reference"
  208. (let [pages (sort (db-model/get-all-page-original-names repo))]
  209. (select pages
  210. (fn [{:keys [value]}]
  211. (append-tree! *tree opts loc [:page-ref value]))
  212. {}))
  213. "full text search"
  214. (search (fn [v] (append-tree! *tree opts loc v))
  215. (:toggle-fn opts))
  216. "between"
  217. (between (merge opts
  218. {:tree *tree
  219. :loc loc
  220. :clause clause}))
  221. nil)]))
  222. (rum/defcs picker <
  223. {:will-mount (fn [state]
  224. (state/clear-selection!)
  225. state)}
  226. (rum/local nil ::mode) ; pick mode
  227. (rum/local nil ::property)
  228. [state *find *tree loc clause opts]
  229. (let [*mode (::mode state)
  230. filters (if (= :page @*find)
  231. query-builder/page-filters
  232. query-builder/block-filters)
  233. filters-and-ops (concat filters query-builder/operators)
  234. operator? #(contains? query-builder/operators-set (keyword %))]
  235. [:div.query-builder-picker
  236. (if @*mode
  237. (when-not (operator? @*mode)
  238. (query-filter-picker state *find *tree loc clause opts))
  239. [:div
  240. (when-not @*find
  241. [:div.flex.flex-row.items-center.p-2.justify-between
  242. [:div.ml-2 "Find: "]
  243. (page-block-selector *find)])
  244. (when-not @*find
  245. [:hr.m-0])
  246. (select
  247. (map name filters-and-ops)
  248. (fn [{:keys [value]}]
  249. (cond
  250. (= value "all page tags")
  251. (append-tree! *tree opts loc [:all-page-tags])
  252. (operator? value)
  253. (append-tree! *tree opts loc [(keyword value)])
  254. :else
  255. (reset! *mode value)))
  256. {:input-default-placeholder "Add filter/operator"})])]))
  257. (rum/defc add-filter
  258. [*find *tree loc clause]
  259. [:a.flex.add-filter
  260. {:title "Add clause"
  261. :on-click (fn [^js e]
  262. (shui/popup-show! (.-target e)
  263. (fn [{:keys [id]}]
  264. (picker *find *tree loc clause {:toggle-fn #(shui/popup-hide! id)}))
  265. {}))}
  266. (ui/icon "plus" {:style {:font-size 20}})])
  267. (declare clauses-group)
  268. (defn- dsl-human-output
  269. [clause]
  270. (let [f (first clause)]
  271. (cond
  272. (string? clause)
  273. (str "search: " clause)
  274. (= (keyword f) :page-ref)
  275. (page-ref/->page-ref (second clause))
  276. (= (keyword f) :page-tags)
  277. (cond
  278. (string? (second clause))
  279. (str "#" (second clause))
  280. (symbol? (second clause))
  281. (str "#" (str (second clause)))
  282. :else
  283. (str "#" (second (second clause))))
  284. (contains? #{:property :page-property} (keyword f))
  285. (str (name (second clause)) ": "
  286. (cond
  287. (and (vector? (last clause)) (= :page-ref (first (last clause))))
  288. (second (last clause))
  289. (= 2 (count clause))
  290. "ALL"
  291. :else
  292. (last clause)))
  293. (= (keyword f) :between)
  294. (let [start (if (or (keyword? (second clause))
  295. (symbol? (second clause)))
  296. (name (second clause))
  297. (second (second clause)))
  298. end (if (or (keyword? (last clause))
  299. (symbol? (last clause)))
  300. (name (last clause))
  301. (second (last clause)))]
  302. (str "between: " start " ~ " end))
  303. (contains? #{:task :priority} (keyword f))
  304. (str (name f) ": "
  305. (string/join " | " (rest clause)))
  306. (contains? #{:page :task :namespace} (keyword f))
  307. (str (name f) ": " (if (vector? (second clause))
  308. (second (second clause))
  309. (second clause)))
  310. (= 2 (count clause))
  311. (str (name f) ": " (second clause))
  312. :else
  313. (str (query-builder/->dsl clause)))))
  314. (rum/defc clause-inner
  315. [*tree loc clause & {:keys [operator?]}]
  316. (ui/dropdown
  317. (fn [{:keys [toggle-fn]}]
  318. (if operator?
  319. [:a.flex.text-sm.query-clause {:on-click toggle-fn}
  320. clause]
  321. [:div.flex.flex-row.items-center.gap-2.p-1.rounded.border.query-clause-btn
  322. [:a.flex.query-clause {:on-click toggle-fn}
  323. (dsl-human-output clause)]]))
  324. (fn [{:keys [toggle-fn]}]
  325. [:div.p-4.flex.flex-col.gap-2
  326. [:a {:title "Delete"
  327. :on-click (fn []
  328. (swap! *tree (fn [q]
  329. (let [loc' (if operator? (vec (butlast loc)) loc)]
  330. (query-builder/remove-element q loc'))))
  331. (toggle-fn))}
  332. "Delete"]
  333. (when operator?
  334. [:a {:title "Unwrap this operator"
  335. :on-click (fn []
  336. (swap! *tree (fn [q]
  337. (let [loc' (vec (butlast loc))]
  338. (query-builder/unwrap-operator q loc'))))
  339. (toggle-fn))}
  340. "Unwrap"])
  341. [:div.font-medium.text-sm "Wrap this filter with: "]
  342. [:div.flex.flex-row.gap-2
  343. (for [op query-builder/operators]
  344. (ui/button (string/upper-case (name op))
  345. :intent "logseq"
  346. :small? true
  347. :on-click (fn []
  348. (swap! *tree (fn [q]
  349. (let [loc' (if operator? (vec (butlast loc)) loc)]
  350. (query-builder/wrap-operator q loc' op))))
  351. (toggle-fn))))]
  352. (when operator?
  353. [:div
  354. [:div.font-medium.text-sm "Replace with: "]
  355. [:div.flex.flex-row.gap-2
  356. (for [op (remove #{(keyword (string/lower-case clause))} query-builder/operators)]
  357. (ui/button (string/upper-case (name op))
  358. :intent "logseq"
  359. :small? true
  360. :on-click (fn []
  361. (swap! *tree (fn [q]
  362. (query-builder/replace-element q loc op)))
  363. (toggle-fn))))]])])
  364. {:modal-class (util/hiccup->class
  365. "origin-top-right.absolute.left-0.mt-2.ml-2.rounded-md.shadow-lg.w-64")}))
  366. (rum/defc clause
  367. [*tree *find loc clause]
  368. (when (seq clause)
  369. [:div.query-builder-clause
  370. (let [kind (keyword (first clause))]
  371. (if (query-builder/operators-set kind)
  372. [:div.operator-clause.flex.flex-row.items-center {:data-level (count loc)}
  373. [:div.clause-bracket "("]
  374. (clauses-group *tree *find (conj loc 0) kind (rest clause))
  375. [:div.clause-bracket ")"]]
  376. (clause-inner *tree loc clause)))]))
  377. (rum/defc clauses-group
  378. [*tree *find loc kind clauses]
  379. (let [parens? (and (= loc [0])
  380. (> (count clauses) 1))]
  381. [:div.clauses-group
  382. (when parens? [:div.clause-bracket "("])
  383. (when-not (and (= loc [0])
  384. (= kind :and)
  385. (<= (count clauses) 1))
  386. (clause-inner *tree loc
  387. (string/upper-case (name kind))
  388. :operator? true))
  389. (map-indexed (fn [i item]
  390. (clause *tree *find (update loc (dec (count loc)) #(+ % i 1)) item))
  391. clauses)
  392. (when parens? [:div.clause-bracket ")"])
  393. (when (not= loc [0])
  394. (add-filter *find *tree loc []))]))
  395. (rum/defc clause-tree < rum/reactive
  396. [*tree *find]
  397. (let [tree (rum/react *tree)
  398. kind ((set query-builder/operators) (first tree))
  399. [kind' clauses] (if kind
  400. [kind (rest tree)]
  401. [:and [@tree]])]
  402. (clauses-group *tree *find [0] kind' clauses)))
  403. (rum/defcs builder <
  404. (rum/local nil ::find)
  405. {:init (fn [state]
  406. (let [q-str (first (:rum/args state))
  407. query (common-util/safe-read-string
  408. query-dsl/custom-readers
  409. (query-dsl/pre-transform-query q-str))
  410. query' (cond
  411. (contains? #{'and 'or 'not} (first query))
  412. query
  413. query
  414. [:and query]
  415. :else
  416. [:and])
  417. tree (query-builder/from-dsl query')
  418. *tree (atom tree)
  419. config (last (:rum/args state))]
  420. (add-watch *tree :updated (fn [_ _ _old _new]
  421. (when-let [block (:block config)]
  422. (let [q (if (= [:and] @*tree)
  423. ""
  424. (let [result (query-builder/->dsl @*tree)]
  425. (if (string? result)
  426. (util/format "\"%s\"" result)
  427. (str result))))
  428. repo (state/get-current-repo)
  429. block (db/pull [:block/uuid (:block/uuid block)])]
  430. (when block
  431. (let [content (string/replace (:block/content block)
  432. #"\{\{query[^}]+\}\}"
  433. (util/format "{{query %s}}" q))]
  434. (editor-handler/save-block! repo (:block/uuid block) content)))))))
  435. (assoc state ::tree *tree)))
  436. :will-mount (fn [state]
  437. (let [q-str (first (:rum/args state))
  438. blocks-query? (:blocks? (query-dsl/parse-query q-str))
  439. find-mode (cond
  440. blocks-query?
  441. :block
  442. (false? blocks-query?)
  443. :page
  444. :else
  445. nil)]
  446. (when find-mode (reset! (::find state) find-mode))
  447. state))}
  448. [state _query _config]
  449. (let [*find (::find state)
  450. *tree (::tree state)]
  451. [:div.cp__query-builder
  452. [:div.cp__query-builder-filter
  453. (when (and (seq @*tree)
  454. (not= @*tree [:and]))
  455. (clause-tree *tree *find))
  456. (add-filter *find *tree [0] [])]]))