1
0

objects.cljs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773
  1. (ns frontend.components.objects
  2. "Tagged objects"
  3. (:require [logseq.shui.ui :as shui]
  4. [rum.core :as rum]
  5. [frontend.util :as util]
  6. [frontend.ui :as ui]
  7. [clojure.string :as string]
  8. [frontend.components.block :as component-block]
  9. [frontend.components.property.value :as pv]
  10. [frontend.components.select :as select]
  11. [frontend.handler.editor :as editor-handler]
  12. [frontend.state :as state]
  13. [frontend.date :as date]
  14. [goog.object :as gobj]
  15. [goog.dom :as gdom]
  16. [cljs-bean.core :as bean]
  17. [promesa.core :as p]
  18. [frontend.db :as db]
  19. [frontend.search.fuzzy :as fuzzy-search]
  20. [logseq.outliner.property :as outliner-property]
  21. [frontend.mixins :as mixins]
  22. [logseq.db.frontend.property :as db-property]
  23. [logseq.db.frontend.property.type :as db-property-type]
  24. [clojure.set :as set]
  25. [datascript.impl.entity :as de]
  26. [cljs-time.core :as t]
  27. [cljs-time.coerce :as tc]
  28. [frontend.db.async :as db-async]))
  29. (defn header-checkbox [{:keys [selected-all? selected-some? toggle-selected-all!]}]
  30. (shui/checkbox
  31. {:checked (or selected-all? (and selected-some? "indeterminate"))
  32. :on-checked-change toggle-selected-all!
  33. :aria-label "Select all"}))
  34. (defn row-checkbox [{:keys [row-selected? row-toggle-selected!]} row _column]
  35. (shui/checkbox
  36. {:checked (row-selected? row)
  37. :on-checked-change (fn [v] (row-toggle-selected! row v))
  38. :aria-label "Select row"}))
  39. (defn- header-cp
  40. [{:keys [column-toggle-sorting! state]} column]
  41. (let [sorting (:sorting state)
  42. [asc?] (some (fn [item] (when (= (:id item) (:id column))
  43. (when-some [asc? (:asc? item)]
  44. [asc?]))) sorting)]
  45. (shui/button
  46. {:variant "text"
  47. :class "!pl-0 !py-0 hover:text-foreground"
  48. :onClick #(column-toggle-sorting! column)}
  49. (:name column)
  50. (case asc?
  51. true
  52. (ui/icon "arrow-up")
  53. false
  54. (ui/icon "arrow-down")
  55. [:div {:style {:width 18 :height 18}}]))))
  56. (defn- timestamp-cell-cp
  57. [_table row column]
  58. (some-> (get row (:id column))
  59. date/int->local-time-2))
  60. (defn- get-property-value-content
  61. [entity]
  62. (when entity
  63. (if (map? entity)
  64. (db-property/property-value-content entity)
  65. (str entity))))
  66. (defn- get-property-value-for-search
  67. [block property]
  68. (let [type (get-in property [:block/schema :type])
  69. many? (= :db.cardinality/many (get property :db/cardinality))
  70. ref-types (into db-property-type/value-ref-property-types #{:entity})
  71. number-type? (= :number type)
  72. v (get block (:db/ident property))
  73. v' (if many? v [v])
  74. col (->> (if (ref-types type) (map db-property/property-value-content v') v')
  75. (remove nil?))]
  76. (if number-type?
  77. (reduce + (filter number? col))
  78. (string/join ", " col))))
  79. (defn- build-columns
  80. [class config]
  81. (let [properties (outliner-property/get-class-properties class)
  82. container-id (state/get-next-container-id)]
  83. (concat
  84. [{:id :select
  85. :name "Select"
  86. :header (fn [table _column] (header-checkbox table))
  87. :cell (fn [table row column] (row-checkbox table row column))
  88. :column-list? false}
  89. {:id :object/name
  90. :name "Name"
  91. :type :string
  92. :header header-cp
  93. :cell (fn [_table row _column]
  94. [:div.primary-cell
  95. (component-block/block-container (assoc config :table? true) row)])
  96. :disable-hide? true}]
  97. (map
  98. (fn [property]
  99. {:id (:db/ident property)
  100. :name (:block/original-name property)
  101. :header header-cp
  102. :cell (fn [_table row _column]
  103. (pv/property-value row property (get row (:db/ident property)) {:container-id container-id}))
  104. :get-value (fn [row] (get-property-value-for-search row property))})
  105. properties)
  106. [{:id :block/created-at
  107. :name "Created At"
  108. :type :date-time
  109. :header header-cp
  110. :cell timestamp-cell-cp}
  111. {:id :block/updated-at
  112. :name "Updated At"
  113. :type :date-time
  114. :header header-cp
  115. :cell timestamp-cell-cp}])))
  116. ;; TODO: block.temp/tagged-at
  117. (defn- get-all-objects
  118. [class]
  119. (->> (:block/_tags class)
  120. (map (fn [row] (assoc row :id (:db/id row))))))
  121. (rum/defc more-actions
  122. [columns {:keys [column-visible? column-toggle-visiblity]}]
  123. (shui/dropdown-menu
  124. (shui/dropdown-menu-trigger
  125. {:asChild true}
  126. (shui/button
  127. {:variant "ghost"
  128. :class "text-muted-foreground !px-1"
  129. :size :sm}
  130. (ui/icon "dots")))
  131. (shui/dropdown-menu-content
  132. {:align "end"}
  133. (shui/dropdown-menu-group
  134. (shui/dropdown-menu-sub
  135. (shui/dropdown-menu-sub-trigger
  136. "Properties")
  137. (shui/dropdown-menu-sub-content
  138. (for [column (remove #(or (false? (:column-list? %))
  139. (:disable-hide? %)) columns)]
  140. (shui/dropdown-menu-checkbox-item
  141. {:key (str (:id column))
  142. :className "capitalize"
  143. :checked (column-visible? column)
  144. :onCheckedChange #(column-toggle-visiblity column %)
  145. :onSelect (fn [e] (.preventDefault e))}
  146. (:name column)))))))))
  147. (defn table-header
  148. [table columns]
  149. (shui/table-row
  150. {:class "bg-gray-01 shadow"}
  151. (for [column columns]
  152. (let [style (case (:id column)
  153. :block/original-name
  154. {}
  155. :select
  156. {:width 32}
  157. {:width 180})]
  158. (shui/table-head
  159. {:key (str (:id column))
  160. :style style}
  161. (let [header-fn (:header column)]
  162. (if (fn? header-fn)
  163. (header-fn table column)
  164. header-fn)))))))
  165. (rum/defc table-row < rum/reactive
  166. [{:keys [row-selected?] :as table} rows columns props]
  167. (let [idx (gobj/get props "data-index")
  168. row (nth rows idx)
  169. row (db/sub-block (:id row))
  170. row (assoc row :id (:db/id row))]
  171. (shui/table-row
  172. (merge
  173. (bean/->clj props)
  174. {:key (str (:id row))
  175. :data-state (when (row-selected? row) "selected")})
  176. (for [column columns]
  177. (let [id (str (:id row) "-" (:id column))
  178. render (get column :cell)]
  179. (shui/table-cell
  180. {:key id}
  181. (render table row column)))))))
  182. (rum/defc search
  183. [input {:keys [on-change set-input!]}]
  184. (let [[show-input? set-show-input!] (rum/use-state false)]
  185. (if show-input?
  186. [:div.flex.flex-row.items-center
  187. (shui/input
  188. {:placeholder "Type to search"
  189. :auto-focus true
  190. :value input
  191. :onChange (fn [e]
  192. (let [value (util/evalue e)]
  193. (on-change value)))
  194. :on-key-down (fn [e]
  195. (when (= "Escape" (util/ekey e))
  196. (set-show-input! false)
  197. (set-input! "")))
  198. :class "max-w-sm !h-7 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"})
  199. (shui/button
  200. {:variant "ghost"
  201. :class "text-muted-foreground !px-1"
  202. :size :sm
  203. :on-click #(do
  204. (set-show-input! false)
  205. (set-input! ""))}
  206. (ui/icon "x"))]
  207. (shui/button
  208. {:variant "ghost"
  209. ;; FIXME: remove ring when focused
  210. :class "text-muted-foreground !px-1"
  211. :size :sm
  212. :on-click #(set-show-input! true)}
  213. (ui/icon "search")))))
  214. (comment
  215. (defn- property-ref-type?
  216. [property]
  217. (let [schema (:block/schema property)
  218. type (:type schema)]
  219. (db-property-type/ref-property-types type))))
  220. (defn- get-property-values
  221. [rows property]
  222. (let [property-ident (:db/ident property)
  223. values (->> (mapcat (fn [e] (let [v (get e property-ident)]
  224. (if (set? v) v #{v}))) rows)
  225. (remove nil?)
  226. (distinct))]
  227. (->>
  228. (map (fn [e]
  229. {:label (get-property-value-content e)
  230. :value e})
  231. values)
  232. (sort-by :label))))
  233. (defn timestamp-property?
  234. [ident]
  235. (contains? #{:block/created-at :block/updated-at} ident))
  236. (def timestamp-options
  237. [{:value "1 week ago"
  238. :label "1 week ago"}
  239. {:value "1 month ago"
  240. :label "1 month ago"}
  241. {:value "3 months ago"
  242. :label "3 months ago"}
  243. {:value "1 year ago"
  244. :label "1 year ago"}
  245. ;; TODO: support date picker
  246. ;; {:value "Custom time"
  247. ;; :label "Custom time"}
  248. ])
  249. (defn- get-timestamp
  250. [value]
  251. (let [now (t/now)
  252. f t/minus]
  253. (case value
  254. "1 week ago"
  255. (tc/to-long (f now (t/weeks 1)))
  256. "1 month ago"
  257. (tc/to-long (f now (t/months 1)))
  258. "3 months ago"
  259. (tc/to-long (f now (t/months 3)))
  260. "1 year ago"
  261. (tc/to-long (f now (t/years 1)))
  262. nil)))
  263. (rum/defc filter-property < rum/static
  264. [columns {:keys [data-fns] :as table}]
  265. (let [[property set-property!] (rum/use-state nil)
  266. timestamp? (timestamp-property? (:db/ident property))
  267. set-filters! (:set-filters! data-fns)
  268. filters (get-in table [:state :filters])
  269. columns (remove #(false? (:column-list? %)) columns)
  270. items (map (fn [column]
  271. {:label (:name column)
  272. :value column}) columns)
  273. option {:input-default-placeholder "Filter"
  274. :input-opts {:class "!px-3 !py-1"}
  275. :items items
  276. :extract-fn :label
  277. :extract-chosen-fn :value
  278. :on-chosen (fn [column]
  279. (let [id (:id column)
  280. property (db/entity id)
  281. internal-property {:db/ident (:id column)
  282. :block/original-name (:name column)}]
  283. (if (or property (timestamp-property? id))
  284. (set-property! (or property internal-property))
  285. (do
  286. (shui/popup-hide!)
  287. (let [property internal-property
  288. new-filter [property :text-contains]
  289. filters' (if (seq filters)
  290. (conj filters new-filter)
  291. [new-filter])]
  292. (set-filters! filters'))))))}
  293. option (cond
  294. timestamp?
  295. (merge option
  296. {:items timestamp-options
  297. :input-default-placeholder (if property (:block/original-name property) "Select")
  298. :on-chosen (fn [value]
  299. (shui/popup-hide!)
  300. (let [filters' (conj filters [property :after value])]
  301. (set-filters! filters')))})
  302. property
  303. (if (= :checkbox (get-in property [:block/schema :type]))
  304. (let [items [{:value true :label "true"}
  305. {:value false :label "false"}]]
  306. (merge option
  307. {:items items
  308. :input-default-placeholder (if property (:block/original-name property) "Select")
  309. :on-chosen (fn [value]
  310. (let [filters' (conj filters [property :is value])]
  311. (set-filters! filters')))}))
  312. (let [items (get-property-values (:data table) property)]
  313. (merge option
  314. {:items items
  315. :input-default-placeholder (if property (:block/original-name property) "Select")
  316. :multiple-choices? true
  317. :on-chosen (fn [_value _selected? selected]
  318. (let [filters' (if (seq selected)
  319. (conj filters [property :is selected])
  320. filters)]
  321. (set-filters! filters')))})))
  322. :else
  323. option)]
  324. (select/select option)))
  325. (rum/defc filter-properties < rum/static
  326. [columns table]
  327. (shui/button
  328. {:variant "ghost"
  329. :class "text-muted-foreground !px-1"
  330. :size :sm
  331. :on-click (fn [e]
  332. (shui/popup-show! (.-target e)
  333. (fn []
  334. (filter-property columns table))
  335. {:align :end
  336. :as-dropdown? true
  337. :auto-focus? true}))}
  338. (ui/icon "filter")))
  339. (defn operator->text
  340. [operator]
  341. (case operator
  342. :is "is"
  343. :is-not "is not"
  344. :text-contains "text contains"
  345. :text-not-contains "text not contains"
  346. :date-before "date before"
  347. :date-after "date after"
  348. :before "before"
  349. :after "after"
  350. :number-gt ">"
  351. :number-lt "<"
  352. :number-gte ">="
  353. :number-lte "<="
  354. :between "between"))
  355. (defn get-property-operators
  356. [property]
  357. (if (contains? #{:block/created-at :block/updated-at} (:db/ident property))
  358. [:before :after]
  359. (concat
  360. [:is :is-not]
  361. (case (get-in property [:block/schema :type])
  362. (:default :url :page :object)
  363. [:text-contains :text-not-contains]
  364. :date
  365. [:date-before :date-after]
  366. :number
  367. [:number-gt :number-lt :number-gte :number-lte :between]
  368. nil))))
  369. (defn- get-filter-with-changed-operator
  370. [property operator value]
  371. (case operator
  372. (:is :is-not)
  373. (when (set? value) value)
  374. (:text-contains :text-not-contains)
  375. (when (string? value) value)
  376. (:number-gt :number-lt :number-gte :number-lte)
  377. (when (number? value) value)
  378. :between
  379. (when (and (vector? value) (every? number? value))
  380. value)
  381. (:date-before :date-after :before :after)
  382. ;; FIXME: should be a valid date number
  383. (when (number? value) value)))
  384. (rum/defc filter-operator < rum/static
  385. [property operator filters set-filters! idx]
  386. (shui/dropdown-menu
  387. (shui/dropdown-menu-trigger
  388. {:asChild true}
  389. (shui/button
  390. {:class "!px-2 rounded-none border-r"
  391. :variant "ghost"
  392. :size :sm}
  393. [:span.text-xs (operator->text operator)]))
  394. (shui/dropdown-menu-content
  395. {:align "start"}
  396. (let [operators (get-property-operators property)]
  397. (for [operator operators]
  398. (shui/dropdown-menu-item
  399. {:on-click (fn []
  400. (let [new-filters (update filters idx
  401. (fn [[property _old-operator value]]
  402. (let [value' (get-filter-with-changed-operator property operator value)]
  403. (if value'
  404. [property operator value']
  405. [property operator]))))]
  406. (set-filters! new-filters)))}
  407. (operator->text operator)))))))
  408. (rum/defc between < rum/static
  409. [property [start end] filters set-filters! idx]
  410. [:<>
  411. (shui/input
  412. {:auto-focus true
  413. :placeholder "from"
  414. :value (str start)
  415. :onChange (fn [e]
  416. (let [input-value (util/evalue e)
  417. number-value (when-not (string/blank? input-value)
  418. (util/safe-parse-float input-value))
  419. value [number-value end]
  420. value (if (every? nil? value) nil value)]
  421. (let [new-filters (update filters idx
  422. (fn [[property operator _old_value]]
  423. (if (nil? value)
  424. [property operator]
  425. [property operator value])))]
  426. (set-filters! new-filters))))
  427. :class "w-24 !h-6 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"})
  428. (shui/input
  429. {:value (str end)
  430. :placeholder "to"
  431. :onChange (fn [e]
  432. (let [input-value (util/evalue e)
  433. number-value (when-not (string/blank? input-value)
  434. (util/safe-parse-float input-value))
  435. value [start number-value]
  436. value (if (every? nil? value) nil value)]
  437. (let [new-filters (update filters idx
  438. (fn [[property operator _old_value]]
  439. (if (nil? value)
  440. [property operator]
  441. [property operator value])))]
  442. (set-filters! new-filters))))
  443. :class "w-24 !h-6 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"})])
  444. (rum/defc filter-value-select < rum/static
  445. [{:keys [data-fns] :as table} property value operator idx]
  446. (let [type (get-in property [:block/schema :type])
  447. items (cond
  448. (contains? #{:before :after} operator)
  449. timestamp-options
  450. (= type :checkbox)
  451. [{:value true :label "true"} {:value false :label "false"}]
  452. :else
  453. (get-property-values (:data table) property))
  454. filters (get-in table [:state :filters])
  455. set-filters! (:set-filters! data-fns)
  456. many? (if (or (contains? #{:date-before :date-after :before :after} operator)
  457. (contains? #{:checkbox} type))
  458. false
  459. true)
  460. option (cond->
  461. {:input-default-placeholder (:block/original-name property)
  462. :input-opts {:class "!px-3 !py-1"}
  463. :items items
  464. :extract-fn :label
  465. :extract-chosen-fn :value
  466. :on-chosen (fn [value _selected? selected]
  467. (when-not many?
  468. (shui/popup-hide!))
  469. (let [value' (if many? selected value)
  470. new-filters (update filters idx
  471. (fn [[property operator _value]]
  472. [property operator value']))]
  473. (set-filters! new-filters)))}
  474. many?
  475. (assoc
  476. :multiple-choices? true
  477. :selected-choices value))]
  478. (shui/dropdown-menu
  479. (shui/dropdown-menu-trigger
  480. {:asChild true}
  481. (shui/button
  482. {:class "!px-2 rounded-none border-r"
  483. :variant "ghost"
  484. :size :sm}
  485. [:div.flex.flex-row.items-center.gap-1.text-xs
  486. (cond
  487. (de/entity? value)
  488. [:div (get-property-value-content value)]
  489. (string? value)
  490. [:div value]
  491. (boolean? value)
  492. [:div (str value)]
  493. (seq value)
  494. (->> (map (fn [v] [:div (get-property-value-content v)]) value)
  495. (interpose [:div "or"]))
  496. :else
  497. "Empty")]))
  498. (shui/dropdown-menu-content
  499. {:align "start"}
  500. (select/select option)))))
  501. (rum/defc filter-value < rum/static
  502. [table property operator value filters set-filters! idx]
  503. (let [number-operator? (string/starts-with? (name operator) "number-")]
  504. (case operator
  505. :between
  506. (between property value filters set-filters! idx)
  507. (:text-contains :text-not-contains :number-gt :number-lt :number-gte :number-lte)
  508. (shui/input
  509. {:auto-focus true
  510. :value (or value "")
  511. :onChange (fn [e]
  512. (let [value (util/evalue e)
  513. number-value (and number-operator? (when-not (string/blank? value)
  514. (util/safe-parse-float value)))]
  515. (let [new-filters (update filters idx
  516. (fn [[property operator _value]]
  517. (if (and number-operator? (nil? number-value))
  518. [property operator]
  519. [property operator (or number-value value)])))]
  520. (set-filters! new-filters))))
  521. :class "w-24 !h-6 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"})
  522. (filter-value-select table property value operator idx))))
  523. (rum/defc filters-row < rum/static
  524. [{:keys [data-fns] :as table}]
  525. (let [filters (get-in table [:state :filters])
  526. {:keys [set-filters!]} data-fns]
  527. (when (seq filters)
  528. [:div.filters-row.flex.flex-row.items-center.gap-4.flex-wrap.pb-2
  529. (map-indexed
  530. (fn [idx filter]
  531. (let [[property operator value] filter]
  532. [:div.flex.flex-row.items-center.border.rounded
  533. (shui/button
  534. {:class "!px-2 rounded-none border-r"
  535. :variant "ghost"
  536. :size :sm
  537. :disabled true}
  538. [:span.text-xs (:block/original-name property)])
  539. (filter-operator property operator filters set-filters! idx)
  540. (filter-value table property operator value filters set-filters! idx)
  541. (shui/button
  542. {:class "!px-1 rounded-none"
  543. :variant "ghost"
  544. :size :sm
  545. :on-click (fn [_e]
  546. (let [new-filters (vec (remove #{filter} filters))]
  547. (set-filters! new-filters)))}
  548. (ui/icon "x"))]))
  549. filters)])))
  550. (defn- fuzzy-matched?
  551. [input s]
  552. (pos? (fuzzy-search/score (string/lower-case (str input))
  553. (string/lower-case (str s)))))
  554. (defn- row-matched?
  555. [row input filters]
  556. (and
  557. ;; full-text-search match
  558. (if (string/blank? input)
  559. true
  560. (when row
  561. (fuzzy-matched? input (:object/name row))))
  562. ;; filters check
  563. (every?
  564. (fn [[property operator match]]
  565. (let [property-ident (:db/ident property)
  566. value (get row property-ident)
  567. value' (cond
  568. (set? value) value
  569. (nil? value) #{}
  570. :else #{value})
  571. result
  572. (case operator
  573. :is
  574. (if (boolean? match)
  575. (= (boolean (get-property-value-content (get row property-ident))) match)
  576. (when (coll? value)
  577. (boolean (seq (set/intersection value' match)))))
  578. :is-not
  579. (if (boolean? match)
  580. (not= (boolean (get-property-value-content (get row property-ident))) match)
  581. (when (coll? value)
  582. (boolean (empty? (set/intersection value' match)))))
  583. :text-contains
  584. (some #(fuzzy-matched? match (get-property-value-content %)) value')
  585. :text-not-contains
  586. (not-any? #(string/includes? (str (get-property-value-content %)) match) value')
  587. :number-gt
  588. (if match (some #(> (get-property-value-content %) match) value') true)
  589. :number-gte
  590. (if match (some #(>= (get-property-value-content %) match) value') true)
  591. :number-lt
  592. (if match (some #(< (get-property-value-content %) match) value') true)
  593. :number-lte
  594. (if match (some #(<= (get-property-value-content %) match) value') true)
  595. :between
  596. (if (seq match)
  597. (some (fn [value-entity]
  598. (let [[start end] match
  599. value (get-property-value-content value-entity)
  600. conditions [(if start (<= start value) true)
  601. (if end (<= value end) true)]]
  602. (if (seq match) (every? true? conditions) true))) value')
  603. true)
  604. :date-before
  605. (if match (some #(< (:block/journal-day %) (:block/journal-day match)) value') true)
  606. :date-after
  607. (if match (some #(> (:block/journal-day %) (:block/journal-day match)) value') true)
  608. :before
  609. (let [search-value (get-timestamp match)]
  610. (if search-value (<= (get row property-ident) search-value) true))
  611. :after
  612. (let [search-value (get-timestamp match)]
  613. (if search-value (>= (get row property-ident) search-value) true))
  614. true)]
  615. result))
  616. filters)))
  617. (defn- add-new-object!
  618. [table class]
  619. (p/let [block (editor-handler/api-insert-new-block! ""
  620. {:page (:block/uuid class)
  621. :properties {:block/tags (:db/id class)}
  622. :edit-block? false})
  623. set-data! (get-in table [:data-fns :set-data!])
  624. _ (set-data! (get-all-objects (db/entity (:db/id class))))]
  625. (editor-handler/edit-block! (db/entity [:block/uuid (:block/uuid block)]) 0 :unknown-container)))
  626. (rum/defc new-record-button < rum/static
  627. [class table]
  628. (ui/tooltip
  629. (shui/button
  630. {:variant "ghost"
  631. :class "!px-1 text-muted-foreground"
  632. :size :sm
  633. :on-click (fn [] (add-new-object! table class))}
  634. (ui/icon "plus"))
  635. [:div "New record"]))
  636. (rum/defc add-new-row < rum/static
  637. [table class]
  638. [:div.p-2.cursor-pointer.flex.flex-row.items-center.gap-1.text-muted-foreground.hover:text-foreground.w-full.text-sm
  639. {:on-click #(add-new-object! table class)}
  640. (ui/icon "plus" {:size 14})
  641. [:div "New"]])
  642. (rum/defc objects-inner < rum/static
  643. [config class]
  644. (let [[input set-input!] (rum/use-state "")
  645. [sorting set-sorting!] (rum/use-state [{:id :block/updated-at, :asc? false}])
  646. [filters set-filters!] (rum/use-state [])
  647. [row-filter set-row-filter!] (rum/use-state nil)
  648. [visible-columns set-visible-columns!] (rum/use-state {})
  649. [row-selection set-row-selection!] (rum/use-state {})
  650. [data set-data!] (rum/use-state [])
  651. _ (rum/use-effect!
  652. (fn []
  653. (p/let [_result (db-async/<get-tag-objects (state/get-current-repo) (:db/id class))]
  654. (set-data! (get-all-objects (db/entity (:db/id class))))))
  655. [])
  656. columns (build-columns class config)
  657. table (shui/table-option {:data data
  658. :columns columns
  659. :state {:sorting sorting
  660. :filters filters
  661. :row-filter row-filter
  662. :row-selection row-selection
  663. :visible-columns visible-columns}
  664. :data-fns {:set-data! set-data!
  665. :set-filters! set-filters!
  666. :set-sorting! set-sorting!
  667. :set-visible-columns! set-visible-columns!
  668. :set-row-selection! set-row-selection!}})
  669. selected-rows (shui/table-get-selection-rows row-selection (:rows table))
  670. selected-rows-count (count selected-rows)
  671. selected? (pos? selected-rows-count)]
  672. (rum/use-effect!
  673. (fn []
  674. (set-row-filter! (fn []
  675. (fn [row]
  676. (row-matched? row input filters)))))
  677. [input filters])
  678. [:div.ls-table.w-full.flex.flex-col.gap-2
  679. [:div.ls-table-header.flex.items-center.justify-between
  680. [:div.flex.flex-row.items-center.gap-2
  681. [:div.font-medium (str (count data) " Objects")]]
  682. [:div.flex.items-center.gap-1
  683. (filter-properties columns table)
  684. (search input {:on-change set-input!
  685. :set-input! set-input!})
  686. (more-actions columns table)
  687. (new-record-button class table)]]
  688. (filters-row table)
  689. (let [columns' (:columns table)
  690. rows (:rows table)]
  691. [:div.ls-table-rows.rounded-md.border.content.overflow-x-auto
  692. (ui/virtualized-table
  693. {:custom-scroll-parent (gdom/getElement "main-content-container")
  694. :total-count (count rows)
  695. :fixedHeaderContent (fn [] (table-header table columns'))
  696. ;; :fixedFooterContent (fn [] (add-new-row table class))
  697. :components {:Table (fn [props]
  698. (shui/table {}
  699. (.-children props)))
  700. :TableRow (fn [props] (table-row table rows columns' props))}})
  701. (add-new-row table class)])
  702. (let [rows-count (count (:rows table))]
  703. [:div.ls-table-footer.flex.items-center.justify-end.space-x-2.py-2
  704. [:div.flex-1.text-sm.text-muted-foreground
  705. (if (pos? selected-rows-count)
  706. (str selected-rows-count " of " rows-count " row(s) selected.")
  707. (str "Total: " rows-count))]])]))
  708. (rum/defcs objects < mixins/container-id
  709. [state class]
  710. (objects-inner {:container-id (:container-id state)} class))