views.cljs 52 KB

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