views.cljs 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296
  1. (ns frontend.components.views
  2. "Different views of blocks"
  3. (:require [cljs-bean.core :as bean]
  4. [cljs-time.coerce :as tc]
  5. [cljs-time.core :as t]
  6. [clojure.set :as set]
  7. [clojure.string :as string]
  8. [datascript.impl.entity :as de]
  9. [frontend.components.dnd :as dnd]
  10. [frontend.components.property.value :as pv]
  11. [frontend.components.select :as select]
  12. [frontend.context.i18n :refer [t]]
  13. [frontend.date :as date]
  14. [frontend.db :as db]
  15. [frontend.handler.property :as property-handler]
  16. [frontend.handler.ui :as ui-handler]
  17. [frontend.state :as state]
  18. [frontend.ui :as ui]
  19. [frontend.util :as util]
  20. [goog.dom :as gdom]
  21. [dommy.core :as dom]
  22. [logseq.db.frontend.property :as db-property]
  23. [logseq.db.frontend.property.type :as db-property-type]
  24. [logseq.shui.ui :as shui]
  25. [rum.core :as rum]
  26. [frontend.mixins :as mixins]
  27. [logseq.shui.table.core :as table-core]
  28. [logseq.db :as ldb]
  29. [frontend.config :as config]
  30. [frontend.db-mixins :as db-mixins]))
  31. (defn- get-latest-entity
  32. [e]
  33. (let [transacted-ids (:updated-ids @(:db/latest-transacted-entity-uuids @state/state))]
  34. (if (and transacted-ids (contains? transacted-ids (:block/uuid e)))
  35. (assoc (db/entity (:db/id e))
  36. :id (:id e)
  37. :block.temp/refs-count (:block.temp/refs-count e))
  38. e)))
  39. (rum/defc header-checkbox < rum/static
  40. [{:keys [selected-all? selected-some? toggle-selected-all!]}]
  41. (let [[show? set-show!] (rum/use-state false)]
  42. [:label.h-8.w-8.flex.items-center.justify-center.cursor-pointer
  43. {:html-for "header-checkbox"
  44. :on-mouse-over #(set-show! true)
  45. :on-mouse-out #(set-show! false)}
  46. (shui/checkbox
  47. {:id "header-checkbox"
  48. :checked (or selected-all? (and selected-some? "indeterminate"))
  49. :on-checked-change toggle-selected-all!
  50. :aria-label "Select all"
  51. :class (str "flex transition-opacity "
  52. (if (or show? selected-all? selected-some?) "opacity-100" "opacity-0"))})]))
  53. (rum/defc row-checkbox < rum/static
  54. [{:keys [row-selected? row-toggle-selected!]} row _column]
  55. (let [id (str (:id row) "-" "checkbox")
  56. [show? set-show!] (rum/use-state false)
  57. checked? (row-selected? row)]
  58. [:label.h-8.w-8.flex.items-center.justify-center.cursor-pointer
  59. {:html-for (str (:id row) "-" "checkbox")
  60. :on-mouse-over #(set-show! true)
  61. :on-mouse-out #(set-show! false)}
  62. (shui/checkbox
  63. {:id id
  64. :checked checked?
  65. :on-checked-change (fn [v] (row-toggle-selected! row v))
  66. :aria-label "Select row"
  67. :class (str "flex transition-opacity "
  68. (if (or show? checked?) "opacity-100" "opacity-0"))})]))
  69. (defn header-cp
  70. [{:keys [column-toggle-sorting! state]} column]
  71. (let [sorting (:sorting state)
  72. [asc?] (some (fn [item] (when (= (:id item) (:id column))
  73. (when-some [asc? (:asc? item)]
  74. [asc?]))) sorting)]
  75. (shui/button
  76. {:variant "text"
  77. :class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start"
  78. :on-click #(column-toggle-sorting! column)}
  79. (let [title (str (:name column))]
  80. [:span {:title title
  81. :class "max-w-full overflow-hidden text-ellipsis"}
  82. title])
  83. (case asc?
  84. true
  85. (ui/icon "arrow-up")
  86. false
  87. (ui/icon "arrow-down")
  88. nil))))
  89. (defn- timestamp-cell-cp
  90. [_table row column]
  91. (some-> (get row (:id column))
  92. date/int->local-time-2))
  93. (defn- get-property-value-content
  94. [entity]
  95. (when entity
  96. (cond
  97. (uuid? entity)
  98. (db-property/property-value-content (db/entity [:block/uuid entity]))
  99. (de/entity? entity)
  100. (db-property/property-value-content entity)
  101. (keyword? entity)
  102. (str entity)
  103. :else
  104. entity)))
  105. (defn- get-property-value-for-search
  106. [block property]
  107. (let [type (get-in property [:block/schema :type])
  108. many? (= :db.cardinality/many (get property :db/cardinality))
  109. number-type? (= :number type)
  110. v (get block (:db/ident property))
  111. v' (if many? v [v])
  112. col (->> (if (db-property-type/all-ref-property-types type) (map db-property/property-value-content v') v')
  113. (remove nil?))]
  114. (if number-type?
  115. (reduce + (filter number? col))
  116. (string/join ", " col))))
  117. (rum/defcs block-container < rum/reactive db-mixins/query
  118. (rum/local false ::deleted?)
  119. [state config row table]
  120. (let [*deleted? (::deleted? state)
  121. container (state/get-component :block/container)
  122. row' (db/sub-block (:db/id row))]
  123. (if (nil? row') ; this row has been deleted
  124. (when-not @*deleted?
  125. (when-let [f (get-in table [:data-fns :set-data!])]
  126. (f (remove (fn [r] (= (:id r) (:id row))) (:data table)))
  127. (reset! *deleted? true)
  128. nil))
  129. [:div.relative.w-full
  130. (container config row')])))
  131. (defn build-columns
  132. [config properties & {:keys [with-object-name? add-tags-column?]
  133. :or {with-object-name? true
  134. add-tags-column? true}}]
  135. (let [;; FIXME: Shouldn't file graphs have :block/tags?
  136. add-tags-column?' (and (config/db-based-graph? (state/get-current-repo)) add-tags-column?)
  137. properties' (->>
  138. (if (or (some #(= (:db/ident %) :block/tags) properties) (not add-tags-column?'))
  139. properties
  140. (conj properties (db/entity :block/tags)))
  141. (remove nil?))]
  142. (->> (concat
  143. [{:id :select
  144. :name "Select"
  145. :header (fn [table _column] (header-checkbox table))
  146. :cell (fn [table row column]
  147. (row-checkbox table row column))
  148. :column-list? false
  149. :resizable? false}
  150. (when with-object-name?
  151. {:id :block/title
  152. :name "Name"
  153. :type :string
  154. :header header-cp
  155. :cell (fn [table row _column]
  156. (block-container (assoc config
  157. :raw-title? (ldb/asset? row)
  158. :table? true)
  159. row
  160. table))
  161. :disable-hide? true})]
  162. (keep
  163. (fn [property]
  164. (when-let [ident (or (:db/ident property) (:id property))]
  165. ;; Hide properties that shouldn't ever be editable or that do not display well in a table
  166. (when-not (or (contains? #{:logseq.property/built-in? :logseq.property.asset/checksum :logseq.property.class/properties
  167. :block/created-at :block/updated-at :block/order :block/collapsed?
  168. :logseq.property/created-from-property}
  169. ident)
  170. (and with-object-name? (= :block/title ident))
  171. (contains? #{:map :entity} (get-in property [:block/schema :type])))
  172. (let [property (if (de/entity? property)
  173. property
  174. (or (merge (db/entity ident) property) property)) ; otherwise, :cell/:header/etc. will be removed
  175. get-value (or (:get-value property)
  176. (when (de/entity? property)
  177. (fn [row] (get-property-value-for-search row property))))
  178. closed-values (seq (:property/closed-values property))
  179. closed-value->sort-number (when closed-values
  180. (->> (zipmap (map :db/id closed-values) (range 0 (count closed-values)))
  181. (into {})))
  182. get-value-for-sort (fn [row]
  183. (cond
  184. (= (:db/ident property) :logseq.task/deadline)
  185. (:block/journal-day (get row :logseq.task/deadline))
  186. closed-values
  187. (closed-value->sort-number (:db/id (get row (:db/ident property))))
  188. :else
  189. (if (fn? get-value)
  190. (get-value row)
  191. (get row ident))))]
  192. {:id ident
  193. :name (or (:name property)
  194. (:block/title property))
  195. :header (or (:header property)
  196. header-cp)
  197. :cell (or (:cell property)
  198. (when (de/entity? property)
  199. (fn [_table row _column]
  200. (pv/property-value row property (get row (:db/ident property)) {}))))
  201. :get-value get-value
  202. :get-value-for-sort get-value-for-sort
  203. :type (:type property)}))))
  204. properties')
  205. [{:id :block/created-at
  206. :name (t :page/created-at)
  207. :type :datetime
  208. :header header-cp
  209. :cell timestamp-cell-cp}
  210. {:id :block/updated-at
  211. :name (t :page/updated-at)
  212. :type :datetime
  213. :header header-cp
  214. :cell timestamp-cell-cp}])
  215. (remove nil?))))
  216. (defn- sort-columns
  217. [columns ordered-column-ids]
  218. (if (seq ordered-column-ids)
  219. (let [id->columns (zipmap (map :id columns) columns)
  220. ordered-id-set (set ordered-column-ids)]
  221. (concat
  222. (keep (fn [id]
  223. (get id->columns id))
  224. ordered-column-ids)
  225. (remove
  226. (fn [column] (ordered-id-set (:id column)))
  227. columns)))
  228. columns))
  229. (rum/defc more-actions
  230. [columns {:keys [column-visible? column-toggle-visibility]}]
  231. (shui/dropdown-menu
  232. (shui/dropdown-menu-trigger
  233. {:asChild true}
  234. (shui/button
  235. {:variant "ghost"
  236. :class "text-muted-foreground !px-1"
  237. :size :sm}
  238. (ui/icon "dots")))
  239. (shui/dropdown-menu-content
  240. {:align "end"}
  241. (shui/dropdown-menu-group
  242. (shui/dropdown-menu-sub
  243. (shui/dropdown-menu-sub-trigger
  244. "Columns visibility")
  245. (shui/dropdown-menu-sub-content
  246. (for [column (remove #(or (false? (:column-list? %))
  247. (:disable-hide? %)) columns)]
  248. (shui/dropdown-menu-checkbox-item
  249. {:key (str (:id column))
  250. :className "capitalize"
  251. :checked (column-visible? column)
  252. :onCheckedChange #(column-toggle-visibility column %)
  253. :onSelect (fn [e] (.preventDefault e))}
  254. (:name column)))))))))
  255. (defn- get-column-size
  256. [column sized-columns]
  257. (let [id (:id column)
  258. size (get sized-columns id)]
  259. (cond
  260. (number? size)
  261. size
  262. (= id :logseq.property/query)
  263. 400
  264. :else
  265. (case id
  266. :select 32
  267. :add-property 160
  268. (:block/title :block/name) 360
  269. (:block/created-at :block/updated-at) 160
  270. 180))))
  271. (rum/defc add-property-button < rum/static
  272. []
  273. [:div.ls-table-header-cell.!border-0
  274. (shui/button
  275. {:variant "text"
  276. :class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start"}
  277. (ui/icon "plus")
  278. "New property")])
  279. (rum/defc action-bar < rum/static
  280. [table selected-rows {:keys [on-delete-rows]}]
  281. (shui/table-actions
  282. {}
  283. (shui/button
  284. {:variant "ghost"
  285. :class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start"
  286. :disabled true}
  287. (str (count selected-rows) " selected"))
  288. (when (fn? on-delete-rows)
  289. (shui/button
  290. {:variant "ghost"
  291. :class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start"
  292. :on-click (fn []
  293. (on-delete-rows table selected-rows))}
  294. (ui/icon "trash")))))
  295. (rum/defc column-resizer
  296. [_column on-sized!]
  297. (let [*el (rum/use-ref nil)
  298. [dx set-dx!] (rum/use-state nil)
  299. [width set-width!] (rum/use-state nil)
  300. add-resizing-class #(dom/add-class! js/document.documentElement "is-resizing-buf")
  301. remove-resizing-class #(dom/remove-class! js/document.documentElement "is-resizing-buf")]
  302. (rum/use-effect!
  303. (fn []
  304. (when (number? dx)
  305. (some-> (rum/deref *el)
  306. (dom/set-style! :transform (str "translate3D(" dx "px , 0, 0)")))))
  307. [dx])
  308. (rum/use-effect!
  309. (fn []
  310. (when-let [el (and (fn? js/window.interact) (rum/deref *el))]
  311. (let [*field-rect (atom nil)
  312. min-width 40
  313. max-width 500]
  314. (-> (js/interact el)
  315. (.draggable
  316. (bean/->js
  317. {:listeners
  318. {:start (fn []
  319. (let [{:keys [width right] :as rect} (bean/->clj (.toJSON (.getBoundingClientRect (.closest el ".ls-table-header-cell"))))
  320. left-dx (if (>= width min-width) (- min-width width) 0)
  321. right-dx (if (<= width max-width) (- max-width width) 0)]
  322. (reset! *field-rect rect)
  323. (swap! *field-rect assoc
  324. ;; calculate left/right boundary
  325. :left-dx left-dx
  326. :right-dx right-dx
  327. :left-b (inc (+ left-dx right))
  328. :right-b (inc (+ right-dx right)))
  329. (dom/add-class! el "is-active")))
  330. :move (fn [^js e]
  331. (let [dx (.-dx e)
  332. pointer-x (js/Math.floor (.-clientX e))
  333. {:keys [left-b right-b]} @*field-rect
  334. left-b (js/Math.floor left-b)
  335. right-b (js/Math.floor right-b)]
  336. (when (and (> pointer-x left-b)
  337. (< pointer-x right-b))
  338. (set-dx! (fn [dx']
  339. (if (contains? #{min-width max-width} (abs dx'))
  340. dx'
  341. (let [to-dx (+ (or dx' 0) dx)
  342. {:keys [left-dx right-dx]} @*field-rect]
  343. (cond
  344. ;; left
  345. (neg? to-dx) (if (> (abs left-dx) (abs to-dx)) to-dx left-dx)
  346. ;; right
  347. (pos? to-dx) (if (> right-dx to-dx) to-dx right-dx)))))))))
  348. :end (fn []
  349. (set-dx!
  350. (fn [dx]
  351. (let [w (js/Math.round (+ dx (:width @*field-rect)))]
  352. (set-width! (cond
  353. (< w min-width) min-width
  354. (> w max-width) max-width
  355. :else w)))
  356. (reset! *field-rect nil)
  357. (dom/remove-class! el "is-active")
  358. 0)))}}))
  359. (.styleCursor false)
  360. (.on "dragstart" add-resizing-class)
  361. (.on "dragend" remove-resizing-class)
  362. (.on "mousedown" util/stop-propagation)))))
  363. [])
  364. (rum/use-effect!
  365. (fn []
  366. (when (number? width)
  367. (on-sized! width)))
  368. [width])
  369. [:a.ls-table-resize-handle
  370. {:data-no-dnd true
  371. :ref *el}]))
  372. (defn- table-header
  373. [table columns {:keys [show-add-property? add-property!] :as option} selected-rows]
  374. (let [set-ordered-columns! (get-in table [:data-fns :set-ordered-columns!])
  375. set-sized-columns! (get-in table [:data-fns :set-sized-columns!])
  376. sized-columns (get-in table [:state :sized-columns])
  377. items (mapv (fn [column]
  378. {:id (:name column)
  379. :value (:id column)
  380. :content (let [header-fn (:header column)
  381. width (get-column-size column sized-columns)
  382. select? (= :select (:id column))]
  383. [:div.ls-table-header-cell
  384. {:style {:width width
  385. :min-width width}
  386. :class (when select? "!border-0")}
  387. (if (fn? header-fn)
  388. (header-fn table column)
  389. header-fn)
  390. ;; resize handle
  391. (when-not (false? (:resizable? column))
  392. (column-resizer column
  393. (fn [size]
  394. (set-sized-columns! (assoc sized-columns (:id column) size)))))])
  395. :disabled? (= (:id column) :select)}) columns)
  396. items (if show-add-property?
  397. (conj items
  398. {:id "add property"
  399. :prop {:style {:width "-webkit-fill-available"
  400. :min-width 160}
  401. :on-click (fn [] (when (fn? add-property!) (add-property!)))}
  402. :value :add-new-property
  403. :content (add-property-button)
  404. :disabled? true})
  405. items)
  406. selection-rows-count (count selected-rows)]
  407. (shui/table-header
  408. (dnd/items items {:vertical? false
  409. :on-drag-end (fn [ordered-columns _m]
  410. (set-ordered-columns! ordered-columns))})
  411. (when (pos? selection-rows-count)
  412. [:div.absolute.top-0.left-8
  413. (action-bar table selected-rows option)]))))
  414. (rum/defc row-cell < rum/static
  415. [table row column render cell-opts idx first-col-rendered? set-first-col-rendered!]
  416. (let [primary-key? (or (= idx 1) (= (:id column) :block/title))]
  417. (when primary-key?
  418. (rum/use-effect!
  419. (fn []
  420. (let [timeout (js/setTimeout #(set-first-col-rendered! true) 0)]
  421. #(js/clearTimeout timeout)))
  422. []))
  423. (shui/table-cell cell-opts
  424. (when (or primary-key? first-col-rendered?)
  425. (render table row column)))))
  426. (rum/defc table-row-inner < rum/static
  427. [{:keys [row-selected?] :as table} row columns props {:keys [show-add-property?]}]
  428. (let [[first-col-rendered? set-first-col-rendered!] (rum/use-state false)
  429. columns (if show-add-property?
  430. (conj (vec columns)
  431. {:id :add-property
  432. :cell (fn [_table _row _column])})
  433. columns)
  434. sized-columns (get-in table [:state :sized-columns])]
  435. (shui/table-row
  436. (merge
  437. props
  438. {:key (str (:id row))
  439. :data-state (when (row-selected? row) "selected")})
  440. (map-indexed
  441. (fn [idx column]
  442. (let [id (str (:id row) "-" (:id column))
  443. render (get column :cell)
  444. width (get-column-size column sized-columns)
  445. select? (= (:id column) :select)
  446. add-property? (= (:id column) :add-property)
  447. cell-opts {:key id
  448. :select? select?
  449. :add-property? add-property?
  450. :style {:width width
  451. :min-width width}}]
  452. (when render
  453. (row-cell table row column render cell-opts idx first-col-rendered? set-first-col-rendered!))))
  454. columns))))
  455. (rum/defc table-row < rum/reactive
  456. [table row columns props option]
  457. (let [row' (db/sub-block (:id row))
  458. ;; merge entity temporal attributes
  459. row (reduce (fn [e [k v]] (assoc e k v)) row' (.-kv ^js row))]
  460. (table-row-inner table row columns props option)))
  461. (rum/defc search
  462. [input {:keys [on-change set-input!]}]
  463. (let [[show-input? set-show-input!] (rum/use-state false)]
  464. (if show-input?
  465. [:div.flex.flex-row.items-center
  466. (shui/input
  467. {:placeholder "Type to search"
  468. :auto-focus true
  469. :value input
  470. :onChange (fn [e]
  471. (let [value (util/evalue e)]
  472. (on-change value)))
  473. :on-key-down (fn [e]
  474. (when (= "Escape" (util/ekey e))
  475. (set-show-input! false)
  476. (set-input! "")))
  477. :class "max-w-sm !h-7 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"})
  478. (shui/button
  479. {:variant "ghost"
  480. :class "text-muted-foreground !px-1"
  481. :size :sm
  482. :on-click #(do
  483. (set-show-input! false)
  484. (set-input! ""))}
  485. (ui/icon "x"))]
  486. (shui/button
  487. {:variant "ghost"
  488. ;; FIXME: remove ring when focused
  489. :class "text-muted-foreground !px-1"
  490. :size :sm
  491. :on-click #(set-show-input! true)}
  492. (ui/icon "search")))))
  493. (comment
  494. (defn- property-ref-type?
  495. [property]
  496. (let [schema (:block/schema property)
  497. type (:type schema)]
  498. (db-property-type/all-ref-property-types type))))
  499. (defn- get-property-values
  500. [rows property]
  501. (let [property-ident (:db/ident property)
  502. values (->> (mapcat (fn [e] (let [e' (db/entity (:db/id e))
  503. v (get e' property-ident)]
  504. (if (set? v) v #{v}))) rows)
  505. (remove nil?)
  506. (distinct))]
  507. (->>
  508. (map (fn [e]
  509. (let [label (get-property-value-content e)]
  510. {:label (str label) :value e}))
  511. values)
  512. (sort-by :label))))
  513. (defn datetime-property?
  514. [property]
  515. (or
  516. (= :datetime (get-in property [:block/schema :type]))
  517. (contains? #{:block/created-at :block/updated-at} (:db/ident property))))
  518. (def timestamp-options
  519. [{:value "1 week ago"
  520. :label "1 week ago"}
  521. {:value "1 month ago"
  522. :label "1 month ago"}
  523. {:value "3 months ago"
  524. :label "3 months ago"}
  525. {:value "1 year ago"
  526. :label "1 year ago"}
  527. ;; TODO: support date picker
  528. ;; {:value "Custom time"
  529. ;; :label "Custom time"}
  530. ])
  531. (defn- get-timestamp
  532. [value]
  533. (let [now (t/now)
  534. f t/minus]
  535. (case value
  536. "1 week ago"
  537. (tc/to-long (f now (t/weeks 1)))
  538. "1 month ago"
  539. (tc/to-long (f now (t/months 1)))
  540. "3 months ago"
  541. (tc/to-long (f now (t/months 3)))
  542. "1 year ago"
  543. (tc/to-long (f now (t/years 1)))
  544. nil)))
  545. (rum/defc filter-property < rum/static
  546. [columns {:keys [data-fns] :as table}]
  547. (let [[property set-property!] (rum/use-state nil)
  548. schema (:schema (db/get-db))
  549. timestamp? (datetime-property? property)
  550. set-filters! (:set-filters! data-fns)
  551. filters (get-in table [:state :filters])
  552. columns (remove #(false? (:column-list? %)) columns)
  553. items (map (fn [column]
  554. {:label (:name column)
  555. :value column}) columns)
  556. option {:input-default-placeholder "Filter"
  557. :input-opts {:class "!px-3 !py-1"}
  558. :items items
  559. :extract-fn :label
  560. :extract-chosen-fn :value
  561. :on-chosen (fn [column]
  562. (let [id (:id column)
  563. property (db/entity id)
  564. internal-property {:db/ident (:id column)
  565. :block/title (:name column)
  566. :block/schema {:type (:type column)}}]
  567. (if (or property
  568. (= :db.cardinality/many (:db/cardinality (get schema id)))
  569. (not= (:type column) :string))
  570. (set-property! (or property internal-property))
  571. (do
  572. (shui/popup-hide!)
  573. (let [property internal-property
  574. new-filter [(:db/ident property) :text-contains]
  575. filters' (if (seq filters)
  576. (conj filters new-filter)
  577. [new-filter])]
  578. (set-filters! filters'))))))}
  579. option (cond
  580. timestamp?
  581. (merge option
  582. {:items timestamp-options
  583. :input-default-placeholder (if property (:block/title property) "Select")
  584. :on-chosen (fn [value]
  585. (shui/popup-hide!)
  586. (let [filters' (conj filters [(:db/ident property) :after value])]
  587. (set-filters! filters')))})
  588. property
  589. (if (= :checkbox (get-in property [:block/schema :type]))
  590. (let [items [{:value true :label "true"}
  591. {:value false :label "false"}]]
  592. (merge option
  593. {:items items
  594. :input-default-placeholder (if property (:block/title property) "Select")
  595. :on-chosen (fn [value]
  596. (let [filters' (conj filters [(:db/ident property) :is value])]
  597. (set-filters! filters')))}))
  598. (let [items (get-property-values (:data table) property)]
  599. (merge option
  600. {:items items
  601. :input-default-placeholder (if property (:block/title property) "Select")
  602. :multiple-choices? true
  603. :on-chosen (fn [_value _selected? selected]
  604. (let [selected-value (if (de/entity? (first selected))
  605. (set (map :block/uuid selected))
  606. selected)
  607. filters' (if (seq selected)
  608. (conj filters [(:db/ident property) :is selected-value])
  609. filters)]
  610. (set-filters! filters')))})))
  611. :else
  612. option)]
  613. (select/select option)))
  614. (rum/defc filter-properties < rum/static
  615. [columns table]
  616. (shui/button
  617. {:variant "ghost"
  618. :class "text-muted-foreground !px-1"
  619. :size :sm
  620. :on-click (fn [e]
  621. (shui/popup-show! (.-target e)
  622. (fn []
  623. (filter-property columns table))
  624. {:align :end
  625. :auto-focus? true}))}
  626. (ui/icon "filter")))
  627. (defn operator->text
  628. [operator]
  629. (case operator
  630. :is "is"
  631. :is-not "is not"
  632. :text-contains "text contains"
  633. :text-not-contains "text not contains"
  634. :date-before "date before"
  635. :date-after "date after"
  636. :before "before"
  637. :after "after"
  638. :number-gt ">"
  639. :number-lt "<"
  640. :number-gte ">="
  641. :number-lte "<="
  642. :between "between"))
  643. (defn get-property-operators
  644. [property]
  645. (if (datetime-property? property)
  646. [:before :after]
  647. (concat
  648. [:is :is-not]
  649. (case (get-in property [:block/schema :type])
  650. (:default :url :node)
  651. [:text-contains :text-not-contains]
  652. (:date)
  653. [:date-before :date-after]
  654. :number
  655. [:number-gt :number-lt :number-gte :number-lte :between]
  656. nil))))
  657. (defn- get-filter-with-changed-operator
  658. [_property operator value]
  659. (case operator
  660. (:is :is-not)
  661. (when (set? value) value)
  662. (:text-contains :text-not-contains)
  663. (when (string? value) value)
  664. (:number-gt :number-lt :number-gte :number-lte)
  665. (when (number? value) value)
  666. :between
  667. (when (and (vector? value) (every? number? value))
  668. value)
  669. (:date-before :date-after :before :after)
  670. ;; FIXME: should be a valid date number
  671. (when (number? value) value)))
  672. (rum/defc filter-operator < rum/static
  673. [property operator filters set-filters! idx]
  674. (shui/dropdown-menu
  675. (shui/dropdown-menu-trigger
  676. {:asChild true}
  677. (shui/button
  678. {:class "!px-2 rounded-none border-r"
  679. :variant "ghost"
  680. :size :sm}
  681. [:span.text-xs (operator->text operator)]))
  682. (shui/dropdown-menu-content
  683. {:align "start"}
  684. (let [operators (get-property-operators property)]
  685. (for [operator operators]
  686. (shui/dropdown-menu-item
  687. {:on-click (fn []
  688. (let [new-filters (update filters idx
  689. (fn [[property _old-operator value]]
  690. (let [value' (get-filter-with-changed-operator property operator value)]
  691. (if value'
  692. [property operator value']
  693. [property operator]))))]
  694. (set-filters! new-filters)))}
  695. (operator->text operator)))))))
  696. (rum/defc between < rum/static
  697. [_property [start end] filters set-filters! idx]
  698. [:<>
  699. (shui/input
  700. {:auto-focus true
  701. :placeholder "from"
  702. :value (str start)
  703. :onChange (fn [e]
  704. (let [input-value (util/evalue e)
  705. number-value (when-not (string/blank? input-value)
  706. (util/safe-parse-float input-value))
  707. value [number-value end]
  708. value (if (every? nil? value) nil value)]
  709. (let [new-filters (update filters idx
  710. (fn [[property operator _old_value]]
  711. (if (nil? value)
  712. [property operator]
  713. [property operator value])))]
  714. (set-filters! new-filters))))
  715. :class "w-24 !h-6 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"})
  716. (shui/input
  717. {:value (str end)
  718. :placeholder "to"
  719. :onChange (fn [e]
  720. (let [input-value (util/evalue e)
  721. number-value (when-not (string/blank? input-value)
  722. (util/safe-parse-float input-value))
  723. value [start number-value]
  724. value (if (every? nil? value) nil value)]
  725. (let [new-filters (update filters idx
  726. (fn [[property operator _old_value]]
  727. (if (nil? value)
  728. [property operator]
  729. [property operator value])))]
  730. (set-filters! new-filters))))
  731. :class "w-24 !h-6 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"})])
  732. (rum/defc filter-value-select < rum/static
  733. [{:keys [data-fns] :as table} property value operator idx]
  734. (let [type (get-in property [:block/schema :type])
  735. items (cond
  736. (contains? #{:before :after} operator)
  737. timestamp-options
  738. (= type :checkbox)
  739. [{:value true :label "true"} {:value false :label "false"}]
  740. :else
  741. (->> (get-property-values (:data table) property)
  742. (map (fn [{:keys [value label]}]
  743. {:label label
  744. :value (or (:block/uuid value) value)}))))
  745. filters (get-in table [:state :filters])
  746. set-filters! (:set-filters! data-fns)
  747. many? (if (or (contains? #{:date-before :date-after :before :after} operator)
  748. (contains? #{:checkbox} type))
  749. false
  750. true)
  751. option (cond->
  752. {:input-default-placeholder (:block/title property)
  753. :input-opts {:class "!px-3 !py-1"}
  754. :items items
  755. :extract-fn :label
  756. :extract-chosen-fn :value
  757. :on-chosen (fn [value _selected? selected]
  758. (when-not many?
  759. (shui/popup-hide!))
  760. (let [value' (if many? selected value)
  761. new-filters (update filters idx
  762. (fn [[property operator _value]]
  763. [property operator value']))]
  764. (set-filters! new-filters)))}
  765. many?
  766. (assoc
  767. :multiple-choices? true
  768. :selected-choices value))]
  769. (shui/dropdown-menu
  770. (shui/dropdown-menu-trigger
  771. {:asChild true}
  772. (shui/button
  773. {:class "!px-2 rounded-none border-r"
  774. :variant "ghost"
  775. :size :sm}
  776. (let [value (cond
  777. (uuid? value)
  778. (db/entity [:block/uuid value])
  779. (and (coll? value) (every? uuid? value))
  780. (set (map #(db/entity [:block/uuid %]) value))
  781. :else
  782. value)]
  783. [:div.flex.flex-row.items-center.gap-1.text-xs
  784. (cond
  785. (de/entity? value)
  786. [:div (get-property-value-content value)]
  787. (string? value)
  788. [:div value]
  789. (boolean? value)
  790. [:div (str value)]
  791. (seq value)
  792. (->> (map (fn [v] [:div (get-property-value-content v)]) value)
  793. (interpose [:div "or"]))
  794. :else
  795. "All")])))
  796. (shui/dropdown-menu-content
  797. {:align "start"}
  798. (select/select option)))))
  799. (rum/defc filter-value < rum/static
  800. [table property operator value filters set-filters! idx]
  801. (let [number-operator? (string/starts-with? (name operator) "number-")]
  802. (case operator
  803. :between
  804. (between property value filters set-filters! idx)
  805. (:text-contains :text-not-contains :number-gt :number-lt :number-gte :number-lte)
  806. (shui/input
  807. {:auto-focus false
  808. :value (or value "")
  809. :onChange (fn [e]
  810. (let [value (util/evalue e)
  811. number-value (and number-operator? (when-not (string/blank? value)
  812. (util/safe-parse-float value)))]
  813. (let [new-filters (update filters idx
  814. (fn [[property operator _value]]
  815. (if (and number-operator? (nil? number-value))
  816. [property operator]
  817. [property operator (or number-value value)])))]
  818. (set-filters! new-filters))))
  819. :class "w-24 !h-6 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"})
  820. (filter-value-select table property value operator idx))))
  821. (rum/defc filters-row < rum/static
  822. [{:keys [data-fns columns] :as table}]
  823. (let [filters (get-in table [:state :filters])
  824. {:keys [set-filters!]} data-fns]
  825. (when (seq filters)
  826. [:div.filters-row.flex.flex-row.items-center.gap-4.flex-wrap.pb-2
  827. (map-indexed
  828. (fn [idx filter']
  829. (let [[property-ident operator value] filter'
  830. property (if (= property-ident :block/title)
  831. {:db/ident property-ident
  832. :block/title "Name"}
  833. (or (db/entity property-ident)
  834. (some (fn [column] (when (= (:id column) property-ident)
  835. {:db/ident (:id column)
  836. :block/title (:name column)})) columns)))]
  837. [:div.flex.flex-row.items-center.border.rounded
  838. (shui/button
  839. {:class "!px-2 rounded-none border-r"
  840. :variant "ghost"
  841. :size :sm
  842. :disabled true}
  843. [:span.text-xs (:block/title property)])
  844. (filter-operator property operator filters set-filters! idx)
  845. (filter-value table property operator value filters set-filters! idx)
  846. (shui/button
  847. {:class "!px-1 rounded-none"
  848. :variant "ghost"
  849. :size :sm
  850. :on-click (fn [_e]
  851. (let [new-filters (vec (remove #{filter'} filters))]
  852. (set-filters! new-filters)))}
  853. (ui/icon "x"))]))
  854. filters)])))
  855. (defn- row-matched?
  856. [row input filters]
  857. (let [row (get-latest-entity row)]
  858. (and
  859. ;; full-text-search match
  860. (if (string/blank? input)
  861. true
  862. (when row
  863. ;; fuzzy search is too slow
  864. (string/includes? (string/lower-case (:block/title row)) (string/lower-case input))))
  865. ;; filters check
  866. (every?
  867. (fn [[property-ident operator match]]
  868. (if (nil? match)
  869. true
  870. (let [value (get row property-ident)
  871. value' (cond
  872. (set? value) value
  873. (nil? value) #{}
  874. :else #{value})
  875. entity? (de/entity? (first value'))
  876. result
  877. (case operator
  878. :is
  879. (if (boolean? match)
  880. (= (boolean (get-property-value-content (get row property-ident))) match)
  881. (cond
  882. (empty? match)
  883. true
  884. (and (empty? match) (empty? value'))
  885. true
  886. :else
  887. (if entity?
  888. (boolean (seq (set/intersection (set (map :block/uuid value')) match)))
  889. (boolean (seq (set/intersection (set value') match))))))
  890. :is-not
  891. (if (boolean? match)
  892. (not= (boolean (get-property-value-content (get row property-ident))) match)
  893. (cond
  894. (and (empty? match) (seq value'))
  895. true
  896. (and (seq match) (empty? value'))
  897. true
  898. :else
  899. (if entity?
  900. (boolean (empty? (set/intersection (set (map :block/uuid value')) match)))
  901. (boolean (empty? (set/intersection (set value') match))))))
  902. :text-contains
  903. (some (fn [v]
  904. (if-let [property-value (get-property-value-content v)]
  905. (string/includes? (string/lower-case property-value) (string/lower-case match))
  906. false))
  907. value')
  908. :text-not-contains
  909. (not-any? #(string/includes? (str (get-property-value-content %)) match) value')
  910. :number-gt
  911. (if match (some #(> (get-property-value-content %) match) value') true)
  912. :number-gte
  913. (if match (some #(>= (get-property-value-content %) match) value') true)
  914. :number-lt
  915. (if match (some #(< (get-property-value-content %) match) value') true)
  916. :number-lte
  917. (if match (some #(<= (get-property-value-content %) match) value') true)
  918. :between
  919. (if (seq match)
  920. (some (fn [value-entity]
  921. (let [[start end] match
  922. value (get-property-value-content value-entity)
  923. conditions [(if start (<= start value) true)
  924. (if end (<= value end) true)]]
  925. (if (seq match) (every? true? conditions) true))) value')
  926. true)
  927. :date-before
  928. (if match (some #(< (:block/journal-day %) (:block/journal-day match)) value') true)
  929. :date-after
  930. (if match (some #(> (:block/journal-day %) (:block/journal-day match)) value') true)
  931. :before
  932. (let [search-value (get-timestamp match)]
  933. (if search-value (<= (get row property-ident) search-value) true))
  934. :after
  935. (let [search-value (get-timestamp match)]
  936. (if search-value (>= (get row property-ident) search-value) true))
  937. true)]
  938. result)))
  939. filters))))
  940. (rum/defc new-record-button < rum/static
  941. [table view-entity]
  942. (let [asset? (and (:logseq.property/built-in? view-entity)
  943. (= (:block/name view-entity) "asset"))]
  944. (ui/tooltip
  945. (shui/button
  946. {:variant "ghost"
  947. :class "!px-1 text-muted-foreground"
  948. :size :sm
  949. :on-click (get-in table [:data-fns :add-new-object!])}
  950. (ui/icon (if asset? "upload" "plus")))
  951. [:div "New record"])))
  952. (rum/defc add-new-row < rum/static
  953. [table]
  954. [:div.py-1.px-2.cursor-pointer.flex.flex-row.items-center.gap-1.text-muted-foreground.hover:text-foreground.w-full.text-sm.border-b
  955. {:on-click (get-in table [:data-fns :add-new-object!])}
  956. (ui/icon "plus" {:size 14})
  957. [:div "New"]])
  958. (defn- table-filters->persist-state
  959. [filters]
  960. (mapv
  961. (fn [[property operator matches]]
  962. (let [matches' (cond
  963. (de/entity? matches)
  964. (:block/uuid matches)
  965. (and (coll? matches) (every? de/entity? matches))
  966. (set (map :block/uuid matches))
  967. :else
  968. matches)]
  969. (if matches'
  970. [property operator matches']
  971. [property operator])))
  972. filters))
  973. (defn- db-set-table-state!
  974. [entity {:keys [set-sorting! set-filters! set-visible-columns!
  975. set-ordered-columns! set-sized-columns!]}]
  976. (let [repo (state/get-current-repo)]
  977. {:set-sorting!
  978. (fn [sorting]
  979. (set-sorting! sorting)
  980. (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/sorting sorting))
  981. :set-filters!
  982. (fn [filters]
  983. (let [filters (table-filters->persist-state filters)]
  984. (set-filters! filters)
  985. (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/filters filters)))
  986. :set-visible-columns!
  987. (fn [columns]
  988. (let [hidden-columns (vec (keep (fn [[column visible?]]
  989. (when (false? visible?)
  990. column)) columns))]
  991. (set-visible-columns! columns)
  992. (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/hidden-columns hidden-columns)))
  993. :set-ordered-columns!
  994. (fn [ordered-columns]
  995. (let [ids (vec (remove #{:select} ordered-columns))]
  996. (set-ordered-columns! ordered-columns)
  997. (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/ordered-columns ids)))
  998. :set-sized-columns!
  999. (fn [sized-columns]
  1000. (set-sized-columns! sized-columns)
  1001. (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/sized-columns sized-columns))}))
  1002. (rum/defc table-view < rum/static
  1003. [table option row-selection add-new-object! *scroller-ref]
  1004. (let [selected-rows (shui/table-get-selection-rows row-selection (:rows table))
  1005. [ready? set-ready?] (rum/use-state false)
  1006. *rows-wrap (rum/use-ref nil)]
  1007. (rum/use-effect!
  1008. (fn [] (set-ready? true))
  1009. [])
  1010. (shui/table
  1011. (let [columns' (:columns table)
  1012. rows (:rows table)]
  1013. [:div.ls-table-rows.content.overflow-x-auto.force-visible-scrollbar
  1014. {:ref *rows-wrap}
  1015. (when ready?
  1016. [:div.relative
  1017. (table-header table columns' option selected-rows)
  1018. (ui/virtualized-list
  1019. {:ref #(reset! *scroller-ref %)
  1020. :custom-scroll-parent (or (some-> (rum/deref *rows-wrap) (.closest ".sidebar-item-list"))
  1021. (gdom/getElement "main-content-container"))
  1022. :increase-viewport-by {:top 300 :bottom 300}
  1023. :compute-item-key (fn [idx]
  1024. (let [block (nth rows idx)]
  1025. (str "table-row-" (:db/id block))))
  1026. :total-count (count rows)
  1027. :item-content (fn [idx]
  1028. (let [row (nth rows idx)]
  1029. (table-row table row columns' {} option)))})
  1030. (when add-new-object!
  1031. (shui/table-footer (add-new-row table)))])]))))
  1032. (rum/defc list-view < rum/static
  1033. [config view-entity result]
  1034. (when-let [->hiccup (state/get-component :block/->hiccup)]
  1035. (let [group-by-page? (not (every? db/page? result))
  1036. result (if group-by-page?
  1037. (group-by :block/page result)
  1038. result)]
  1039. (->hiccup result
  1040. (assoc config
  1041. :custom-query? true
  1042. :current-block (:db/id view-entity)
  1043. :query (:block/title view-entity)
  1044. :breadcrumb-show? (if group-by-page? true false)
  1045. :group-by-page? group-by-page?
  1046. :ref? true)))))
  1047. (rum/defc gallery-card-item
  1048. [table view-entity block config]
  1049. [:div.ls-card-item.content
  1050. {:key (str "view-card-" (:db/id view-entity) "-" (:db/id block))}
  1051. [:div.-ml-4
  1052. (block-container (assoc config
  1053. :id (str (:block/uuid block))
  1054. :gallery-view? true)
  1055. block
  1056. table)]])
  1057. (rum/defcs gallery-view < rum/static mixins/container-id
  1058. [state config table view-entity blocks *scroller-ref]
  1059. (let [config' (assoc config :container-id (:container-id state))]
  1060. [:div.ls-cards
  1061. (when (seq blocks)
  1062. (ui/virtualized-grid
  1063. {:ref #(reset! *scroller-ref %)
  1064. :total-count (count blocks)
  1065. :custom-scroll-parent (gdom/getElement "main-content-container")
  1066. :item-content (fn [idx]
  1067. (when-let [block (nth blocks idx)]
  1068. (gallery-card-item table view-entity block config')))}))]))
  1069. (defn- run-effects!
  1070. [option {:keys [data columns state data-fns]} input input-filters set-input-filters! *scroller-ref gallery?]
  1071. (let [{:keys [filters sorting]} state
  1072. {:keys [set-row-filter! set-data!]} data-fns]
  1073. (rum/use-effect!
  1074. (fn []
  1075. (let [new-input-filters [input filters]]
  1076. (when-not (= input-filters new-input-filters)
  1077. (set-input-filters! [input filters])
  1078. (set-row-filter!
  1079. (fn []
  1080. (fn [row]
  1081. (row-matched? row input filters)))))))
  1082. [input filters])
  1083. (rum/use-effect!
  1084. (fn []
  1085. ;; Entities might be outdated
  1086. (let [;; TODO: should avoid this for better performance, 300ms for 40k pages
  1087. new-data (map get-latest-entity data)
  1088. ;; TODO: db support native order-by, limit, offset, 350ms for 40k pages
  1089. data' (table-core/table-sort-rows new-data sorting columns)]
  1090. (when (not= data' data)
  1091. (set-data! data'))
  1092. (when (and (:current-page? (:config option)) (seq data) (map? (first data)) (:block/uuid (first data)))
  1093. (ui-handler/scroll-to-anchor-block @*scroller-ref data' gallery?)
  1094. (state/set-state! :editor/virtualized-scroll-fn #(ui-handler/scroll-to-anchor-block @*scroller-ref data' gallery?)))))
  1095. [sorting data])))
  1096. (rum/defc view-inner < rum/static
  1097. [view-entity {:keys [data set-data! columns add-new-object! views-title title-key render-empty-title?] :as option
  1098. :or {render-empty-title? false}}
  1099. *scroller-ref]
  1100. (let [[input set-input!] (rum/use-state "")
  1101. sorting (:logseq.property.table/sorting view-entity)
  1102. [sorting set-sorting!] (rum/use-state (or sorting [{:id :block/updated-at, :asc? false}]))
  1103. filters (:logseq.property.table/filters view-entity)
  1104. [filters set-filters!] (rum/use-state (or filters []))
  1105. default-visible-columns (if-let [hidden-columns (:logseq.property.table/hidden-columns view-entity)]
  1106. (zipmap hidden-columns (repeat false))
  1107. ;; This case can happen for imported tables
  1108. (if (seq (:logseq.property.table/ordered-columns view-entity))
  1109. (zipmap (set/difference (set (map :id columns))
  1110. (set (:logseq.property.table/ordered-columns view-entity))
  1111. #{:select :block/created-at :block/updated-at})
  1112. (repeat false))
  1113. {}))
  1114. [visible-columns set-visible-columns!] (rum/use-state default-visible-columns)
  1115. ordered-columns (vec (concat [:select] (:logseq.property.table/ordered-columns view-entity)))
  1116. sized-columns (:logseq.property.table/sized-columns view-entity)
  1117. [ordered-columns set-ordered-columns!] (rum/use-state ordered-columns)
  1118. [sized-columns set-sized-columns!] (rum/use-state sized-columns)
  1119. {:keys [set-sorting! set-filters! set-visible-columns! set-ordered-columns! set-sized-columns!]}
  1120. (db-set-table-state! view-entity {:set-sorting! set-sorting!
  1121. :set-filters! set-filters!
  1122. :set-visible-columns! set-visible-columns!
  1123. :set-sized-columns! set-sized-columns!
  1124. :set-ordered-columns! set-ordered-columns!})
  1125. row-filter-fn (fn []
  1126. (fn [row]
  1127. (row-matched? row input filters)))
  1128. [row-filter set-row-filter!] (rum/use-state row-filter-fn)
  1129. [input-filters set-input-filters!] (rum/use-state [input filters])
  1130. [row-selection set-row-selection!] (rum/use-state {})
  1131. columns (sort-columns columns ordered-columns)
  1132. table-map {:data data
  1133. :columns columns
  1134. :state {:sorting sorting
  1135. :filters filters
  1136. :row-filter row-filter
  1137. :row-selection row-selection
  1138. :visible-columns visible-columns
  1139. :sized-columns sized-columns
  1140. :ordered-columns ordered-columns}
  1141. :data-fns {:set-data! set-data!
  1142. :set-row-filter! set-row-filter!
  1143. :set-filters! set-filters!
  1144. :set-sorting! set-sorting!
  1145. :set-visible-columns! set-visible-columns!
  1146. :set-ordered-columns! set-ordered-columns!
  1147. :set-sized-columns! set-sized-columns!
  1148. :set-row-selection! set-row-selection!
  1149. :add-new-object! add-new-object!}}
  1150. table (shui/table-option table-map)
  1151. *view-ref (rum/use-ref nil)
  1152. display-type (or (:db/ident (get view-entity :logseq.property.view/type))
  1153. :logseq.property.view/type.table)
  1154. gallery? (= display-type :logseq.property.view/type.gallery)]
  1155. (run-effects! option table-map input input-filters set-input-filters! *scroller-ref gallery?)
  1156. [:div.flex.flex-col.gap-2.grid
  1157. {:ref *view-ref}
  1158. [:div.flex.flex-wrap.items-center.justify-between.gap-1
  1159. (when-not render-empty-title?
  1160. [:div.flex.flex-row.items-center.gap-2
  1161. (or
  1162. views-title
  1163. [:div.font-medium.opacity-50.text-sm
  1164. (t (or title-key :views.table/default-title)
  1165. (count (:rows table)))])])
  1166. [:div.view-actions.flex.items-center.gap-1
  1167. (filter-properties columns table)
  1168. (search input {:on-change set-input!
  1169. :set-input! set-input!})
  1170. [:div.text-muted-foreground.text-sm
  1171. (pv/property-value view-entity (db/entity :logseq.property.view/type)
  1172. (db/entity display-type) {})]
  1173. (more-actions columns table)
  1174. (when add-new-object! (new-record-button table view-entity))]]
  1175. (filters-row table)
  1176. (case display-type
  1177. :logseq.property.view/type.list
  1178. (list-view (:config option) view-entity (:rows table))
  1179. :logseq.property.view/type.gallery
  1180. (gallery-view (:config option) table view-entity (:rows table) *scroller-ref)
  1181. (table-view table option row-selection add-new-object! *scroller-ref))]))
  1182. (rum/defcs view
  1183. "Provides a view for data like query results and tagged objects, multiple
  1184. layouts such as table and list are supported. Args:
  1185. * view-entity: a db Entity
  1186. * option:
  1187. * title-key: dict key defaults to `:views.table/default-title`
  1188. * data: a collections of entities
  1189. * set-data!: `fn` to update `data`
  1190. * columns: view columns including properties and db attributes, which could be built by `build-columns`
  1191. * add-new-object!: `fn` to create a new object (or row)
  1192. * show-add-property?: whether to show `Add property`
  1193. * add-property!: `fn` to add a new property (or column)
  1194. * on-delete-rows: `fn` to trigger when deleting selected objects"
  1195. < rum/reactive
  1196. (rum/local nil ::scroller-ref)
  1197. [state view-entity option]
  1198. (let [view-entity' (db/sub-block (:db/id view-entity))]
  1199. (rum/with-key (view-inner view-entity'
  1200. (cond-> option
  1201. config/publishing?
  1202. (dissoc :add-new-object!))
  1203. (::scroller-ref state))
  1204. (str "view-" (:db/id view-entity')))))