builder.cljs 18 KB

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