builder.cljs 19 KB

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