query_table.cljs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. (ns frontend.components.file-based.query-table
  2. (:require [clojure.string :as string]
  3. [frontend.components.svg :as svg]
  4. [frontend.date :as date]
  5. [frontend.db :as db]
  6. [frontend.db.query-dsl :as query-dsl]
  7. [frontend.format.block :as block]
  8. [frontend.handler.common :as common-handler]
  9. [frontend.handler.file-based.property :as file-property-handler]
  10. [frontend.handler.property :as property-handler]
  11. [frontend.state :as state]
  12. [frontend.util :as util]
  13. [frontend.util.file-based.clock :as clock]
  14. [logseq.graph-parser.text :as text]
  15. [medley.core :as medley]
  16. [promesa.core :as p]
  17. [rum.core :as rum]))
  18. ;; Util fns
  19. ;; ========
  20. (defn- attach-clock-property
  21. [result]
  22. ;; FIXME: Look up by property id if still supporting clock-time
  23. (let [ks [:block/properties :clock-time]
  24. result (map (fn [b]
  25. (let [b (update (block/parse-title-and-body b)
  26. :block/properties (fn [properties] (if (map? properties) properties {})))]
  27. (try (assoc-in b ks (or (clock/clock-summary (:block.temp/ast-body b) false) 0))
  28. (catch :default _e
  29. b))))
  30. result)]
  31. (if (every? #(zero? (get-in % ks)) result)
  32. (map #(medley/dissoc-in % ks) result)
  33. result)))
  34. (defn- sort-by-fn [sort-by-column item {:keys [page?]}]
  35. (case sort-by-column
  36. :created-at
  37. (:block/created-at item)
  38. :updated-at
  39. (:block/updated-at item)
  40. :block
  41. (:block/title item)
  42. :page
  43. (if page? (:block/name item) (get-in item [:block/page :block/name]))
  44. (get-in item [:block/properties sort-by-column])))
  45. (defn- locale-compare
  46. "Use locale specific comparison for strings and general comparison for others."
  47. [x y]
  48. (if (and (number? x) (number? y))
  49. (< x y)
  50. (.localeCompare (str x) (str y) (state/sub :preferred-language) #js {:numeric true})))
  51. (defn- sort-result [result {:keys [sort-by-column sort-desc? sort-nlp-date? page?]}]
  52. (if (some? sort-by-column)
  53. (let [comp-fn (if sort-desc? #(locale-compare %2 %1) locale-compare)]
  54. (sort-by (fn [item]
  55. (block/normalize-block (sort-by-fn sort-by-column item {:page? page?})
  56. sort-nlp-date?))
  57. comp-fn
  58. result))
  59. result))
  60. (defn- get-sort-state
  61. "Return current sort direction and column being sorted, respectively
  62. :sort-desc? and :sort-by-column. :sort-by-column is nil if no sorting is to be
  63. done"
  64. [current-block]
  65. (let [properties (:block/properties current-block)
  66. p-desc? (:query-sort-desc properties)
  67. desc? (if (some? p-desc?) p-desc? true)
  68. properties (:block/properties current-block)
  69. query-sort-by (:query-sort-by properties)
  70. ;; Starting with #6105, we started putting properties under namespaces.
  71. nlp-date? (:logseq.query/nlp-date properties)
  72. sort-by-column (or (keyword query-sort-by)
  73. (if (query-dsl/query-contains-filter? (:block/title current-block) "sort-by")
  74. nil
  75. :updated-at))]
  76. {:sort-desc? desc?
  77. :sort-by-column sort-by-column
  78. :sort-nlp-date? nlp-date?}))
  79. ;; Components
  80. ;; ==========
  81. (rum/defc sortable-title
  82. [title column {:keys [sort-by-column sort-desc?]} block-id]
  83. (let [repo (state/get-current-repo)]
  84. [:th.whitespace-nowrap
  85. [:a {:on-click (fn []
  86. (p/do!
  87. (property-handler/set-block-property! repo block-id
  88. :query-sort-by
  89. (name column))
  90. (property-handler/set-block-property! repo block-id
  91. :query-sort-desc
  92. (not sort-desc?))))}
  93. [:div.flex.items-center
  94. [:span.mr-1 title]
  95. (when (= sort-by-column column)
  96. [:span
  97. (if sort-desc? (svg/caret-down) (svg/caret-up))])]]]))
  98. (defn get-all-columns-for-result
  99. "Gets all possible columns for a given result. Property names are keywords"
  100. [result page?]
  101. (let [hidden-properties (conj (file-property-handler/built-in-properties) :template)
  102. prop-keys* (->> (distinct (mapcat keys (map :block/properties result)))
  103. (remove hidden-properties))
  104. prop-keys (cond-> (if page? (cons :page prop-keys*) (concat '(:block :page) prop-keys*))
  105. page?
  106. (concat [:created-at :updated-at]))]
  107. prop-keys))
  108. (defn get-columns [current-block result {:keys [page?]}]
  109. (let [properties (:block/properties current-block)
  110. query-properties (some-> (:query-properties properties)
  111. (common-handler/safe-read-string "Parsing query properties failed"))
  112. query-properties (if page? (remove #{:block} query-properties) query-properties)
  113. columns (if (seq query-properties)
  114. query-properties
  115. (get-all-columns-for-result result page?))]
  116. (distinct columns)))
  117. (defn- build-column-value
  118. "Builds a column's tuple value for a query table given a row, column and
  119. options"
  120. [row column {:keys [page? ->elem map-inline comma-separated-property?]}]
  121. (case column
  122. :page
  123. [:string (if page?
  124. (or (:block/title row)
  125. (:block/name row))
  126. (or (get-in row [:block/page :block/title])
  127. (get-in row [:block/page :block/name])))]
  128. :block ; block title
  129. (let [content (:block/title row)
  130. uuid (:block/uuid row)
  131. {:block/keys [title]} (block/parse-title-and-body
  132. (:block/uuid row)
  133. (get row :block/format :markdown)
  134. (:block/pre-block? row)
  135. content)]
  136. (if (seq title)
  137. [:element (->elem :div (map-inline {:block/uuid uuid} title))]
  138. [:string content]))
  139. :created-at
  140. [:string (when-let [created-at (:block/created-at row)]
  141. (date/int->local-time-2 created-at))]
  142. :updated-at
  143. [:string (when-let [updated-at (:block/updated-at row)]
  144. (date/int->local-time-2 updated-at))]
  145. [:string
  146. (let [value (get-in row [:block/properties column])]
  147. (if (or comma-separated-property? (coll? value))
  148. ;; Return original properties since comma properties need to
  149. ;; return collections for display purposes
  150. value
  151. (or (get-in row [:block/properties-text-values column])
  152. ;; Fallback to original properties for page blocks
  153. value)))]))
  154. (defn- render-column-value
  155. [{:keys [row-block row-format cell-format value]} page-cp inline-text]
  156. (cond
  157. ;; elements should be rendered as they are provided
  158. (= :element cell-format) value
  159. (coll? value) (->> (map #(page-cp {} {:block/name %}) value)
  160. (interpose [:span ", "]))
  161. ;; boolean values need to first be stringified
  162. (boolean? value) (str value)
  163. ;; string values will attempt to be rendered as pages, falling back to
  164. ;; inline-text when no page entity is found
  165. (string? value) (if-let [page (and (string? value) (db/get-page value))]
  166. (page-cp {:stop-event-propagation? true} page)
  167. (inline-text row-block row-format value))
  168. ;; anything else should just be rendered as provided
  169. :else value))
  170. (rum/defc table-row
  171. [row columns config page? select? *mouse-down? map-inline page-cp ->elem inline-text]
  172. (let [format (get row :block/format :markdown)
  173. property-separated-by-commas? (partial text/separated-by-commas? (state/get-config))]
  174. [:tr.cursor
  175. (for [column columns]
  176. (let [[cell-format value] (build-column-value row
  177. column
  178. {:page? page?
  179. :->elem ->elem
  180. :map-inline map-inline
  181. :config config
  182. :comma-separated-property? (property-separated-by-commas? column)})]
  183. [:td.whitespace-nowrap
  184. {:data-key (pr-str column)
  185. :on-pointer-down (fn []
  186. (reset! *mouse-down? true)
  187. (reset! select? false))
  188. :on-mouse-move (fn [] (reset! select? true))
  189. :on-pointer-up (fn []
  190. (when (and @*mouse-down? (not @select?))
  191. (state/sidebar-add-block!
  192. (state/get-current-repo)
  193. (:db/id row)
  194. :block)
  195. (reset! *mouse-down? false)))}
  196. (when (some? value)
  197. (render-column-value {:row-block row
  198. :row-format format
  199. :cell-format cell-format
  200. :value value}
  201. page-cp
  202. inline-text))]))]))
  203. (rum/defcs result-table-v1 < rum/reactive
  204. (rum/local false ::select?)
  205. (rum/local false ::mouse-down?)
  206. [state config current-block sort-result' sort-state columns {:keys [page?]} map-inline page-cp ->elem inline-text]
  207. (let [select? (get state ::select?)
  208. *mouse-down? (::mouse-down? state)
  209. clock-time-total (when-not page?
  210. (->> (map #(get-in % [:block/properties :clock-time] 0) sort-result')
  211. (apply +)))]
  212. [:div.overflow-x-auto {:on-pointer-down (fn [e] (.stopPropagation e))
  213. :style {:width "100%"}
  214. :class "query-table"}
  215. [:table.table-auto
  216. [:thead
  217. [:tr.cursor
  218. (for [column columns]
  219. (let [title (if (and (= column :clock-time) (integer? clock-time-total))
  220. (util/format "clock-time(total: %s)" (clock/seconds->days:hours:minutes:seconds
  221. clock-time-total))
  222. (name column))]
  223. (sortable-title title column sort-state (:block/uuid current-block))))]]
  224. [:tbody
  225. (for [row sort-result']
  226. (table-row row columns config page? select? *mouse-down? map-inline page-cp ->elem inline-text))]]]))
  227. (rum/defc result-table < rum/reactive
  228. [config current-block result {:keys [page?] :as options} map-inline page-cp ->elem inline-text]
  229. (when current-block
  230. (let [result (map (fn [item]
  231. (if (and (map? item) (:db/id item))
  232. (if-let [entity-title (:block/title (db/entity (:db/id item)))]
  233. (assoc item :block/title entity-title)
  234. (update item :block/title (fn [title]
  235. (some-> title
  236. (string/replace "$pfts_2lqh>$" "")
  237. (string/replace "$<pfts_2lqh$" "")))))
  238. item))
  239. result)
  240. result' (if page? result (attach-clock-property result))
  241. columns (get-columns current-block result' {:page? page?})
  242. ;; Sort state needs to be in sync between final result and sortable title
  243. ;; as user needs to know if there result is sorted
  244. sort-state (get-sort-state current-block)
  245. sort-result' (sort-result result' (assoc sort-state :page? page?))]
  246. (result-table-v1 config current-block sort-result' sort-state columns options map-inline page-cp ->elem inline-text))))