builder.cljs 22 KB

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