query_table.cljs 15 KB

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