1
0

query_table.cljs 12 KB

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