views.cljs 89 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893
  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. [cljs-time.format :as tf]
  7. [clojure.set :as set]
  8. [clojure.string :as string]
  9. [datascript.impl.entity :as de]
  10. [dommy.core :as dom]
  11. [frontend.common.missionary :as c.m]
  12. [frontend.components.dnd :as dnd]
  13. [frontend.components.icon :as icon-component]
  14. [frontend.components.property.config :as property-config]
  15. [frontend.components.property.value :as pv]
  16. [frontend.components.select :as select]
  17. [frontend.components.selection :as selection]
  18. [frontend.config :as config]
  19. [frontend.context.i18n :refer [t]]
  20. [frontend.date :as date]
  21. [frontend.db :as db]
  22. [frontend.db-mixins :as db-mixins]
  23. [frontend.db.async :as db-async]
  24. [frontend.handler.db-based.export :as db-export-handler]
  25. [frontend.handler.db-based.property :as db-property-handler]
  26. [frontend.handler.editor :as editor-handler]
  27. [frontend.handler.property :as property-handler]
  28. [frontend.handler.property.util :as pu]
  29. [frontend.handler.route :as route-handler]
  30. [frontend.handler.ui :as ui-handler]
  31. [frontend.mixins :as mixins]
  32. [frontend.modules.outliner.op :as outliner-op]
  33. [frontend.modules.outliner.ui :as ui-outliner-tx]
  34. [frontend.state :as state]
  35. [frontend.ui :as ui]
  36. [frontend.util :as util]
  37. [goog.dom :as gdom]
  38. [goog.object :as gobj]
  39. [logseq.common.config :as common-config]
  40. [logseq.db :as ldb]
  41. [logseq.db.common.view :as db-view]
  42. [logseq.db.frontend.property :as db-property]
  43. [logseq.shui.hooks :as hooks]
  44. [logseq.shui.ui :as shui]
  45. [medley.core :as medley]
  46. [missionary.core :as m]
  47. [promesa.core :as p]
  48. [rum.core :as rum]))
  49. (rum/defc header-checkbox < rum/static
  50. [{:keys [selected-all? selected-some? toggle-selected-all!] :as table}]
  51. (let [[show? set-show!] (rum/use-state false)]
  52. [:label.h-8.w-8.flex.items-center.justify-center.cursor-pointer
  53. {:html-for "header-checkbox"
  54. :on-mouse-over #(set-show! true)
  55. :on-mouse-out #(set-show! false)}
  56. (shui/checkbox
  57. {:id "header-checkbox"
  58. :checked (or selected-all? (and selected-some? "indeterminate"))
  59. :on-checked-change (fn [value]
  60. (p/do
  61. (when value
  62. (db-async/<get-blocks (state/get-current-repo) (:rows table) {}))
  63. (toggle-selected-all! table value)))
  64. :aria-label "Select all"
  65. :class (str "flex transition-opacity "
  66. (if (or show? selected-all? selected-some?) "opacity-100" "opacity-0"))})]))
  67. (rum/defc header-index < rum/static
  68. []
  69. [:label.h-8.w-6.flex.items-center.justify-center
  70. {:html-for "header-index"
  71. :title "Row number"}
  72. "ID"])
  73. (rum/defc row-checkbox < rum/static
  74. [{:keys [row-selected? row-toggle-selected!]} row _column]
  75. (let [id (str (:db/id row) "-" "checkbox")
  76. [show? set-show!] (rum/use-state false)
  77. checked? (row-selected? row)]
  78. [:label.h-8.w-8.flex.items-center.justify-center.cursor-pointer
  79. {:html-for (str (:db/id row) "-" "checkbox")
  80. :on-mouse-over #(set-show! true)
  81. :on-mouse-out #(set-show! false)}
  82. (shui/checkbox
  83. {:id id
  84. :checked checked?
  85. :on-checked-change (fn [v]
  86. (p/do!
  87. (when v (db-async/<get-block (state/get-current-repo) (:db/id row) {:skip-refresh? true
  88. :children? false}))
  89. (row-toggle-selected! row v)))
  90. :aria-label "Select row"
  91. :class (str "flex transition-opacity "
  92. (if (or show? checked?) "opacity-100" "opacity-0"))})]))
  93. (defonce *last-header-action-target (atom nil))
  94. (defn header-cp
  95. [{:keys [view-entity column-set-sorting! state]} column]
  96. (let [sorting (:sorting state)
  97. db-based? (config/db-based-graph?)
  98. [asc?] (some (fn [item] (when (= (:id item) (:id column))
  99. (when-some [asc? (:asc? item)]
  100. [asc?]))) sorting)
  101. property (db/entity (:id column))
  102. pinned? (when property
  103. (contains? (set (map :db/id (:logseq.property.table/pinned-columns view-entity)))
  104. (:db/id property)))
  105. sub-content (fn [{:keys [id]}]
  106. [:<>
  107. (shui/dropdown-menu-item
  108. {:key "asc"
  109. :on-click #(column-set-sorting! sorting column true)}
  110. [:div.flex.flex-row.items-center.gap-1
  111. (ui/icon "arrow-up" {:size 15})
  112. [:div "Sort ascending"]])
  113. (shui/dropdown-menu-item
  114. {:key "desc"
  115. :on-click #(column-set-sorting! sorting column false)}
  116. [:div.flex.flex-row.items-center.gap-1
  117. (ui/icon "arrow-down" {:size 15})
  118. [:div "Sort descending"]])
  119. (when property
  120. (shui/dropdown-menu-item
  121. {:on-click #(shui/popup-show! (.-target %)
  122. (fn []
  123. [:div.ls-property-dropdown-editor.-m-1
  124. (property-config/dropdown-editor property nil {})])
  125. {:align "start"})}
  126. [:div.flex.flex-row.items-center.gap-1
  127. (ui/icon "adjustments" {:size 15}) "Configure"]))
  128. (when (and db-based? property)
  129. (shui/dropdown-menu-item
  130. {:on-click (fn [_e]
  131. (if pinned?
  132. (db-property-handler/delete-property-value! (:db/id view-entity)
  133. :logseq.property.table/pinned-columns
  134. (:db/id property))
  135. (property-handler/set-block-property! (state/get-current-repo)
  136. (:db/id view-entity)
  137. :logseq.property.table/pinned-columns
  138. (:db/id property)))
  139. (shui/popup-hide! id))}
  140. [:div.flex.flex-row.items-center.gap-1
  141. (ui/icon "pin" {:size 15})
  142. [:div (if pinned? "Unpin" "Pin")]]))])]
  143. (shui/button
  144. {:variant "text"
  145. :class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start"
  146. :on-mouse-up (fn [^js e]
  147. (when-let [^js el (some-> (.-target e) (.closest "[aria-roledescription=sortable]"))]
  148. (when (and (or (nil? @*last-header-action-target)
  149. (not= el @*last-header-action-target))
  150. (string/blank? (some-> el (.-style) (.-transform))))
  151. (shui/popup-show! el sub-content
  152. {:align "start" :as-dropdown? true
  153. :on-before-hide (fn []
  154. (reset! *last-header-action-target el)
  155. (js/setTimeout #(reset! *last-header-action-target nil) 128))}))))}
  156. (let [title (str (:name column))]
  157. [:span {:title title
  158. :class "max-w-full overflow-hidden text-ellipsis"}
  159. title])
  160. (case asc?
  161. true
  162. (ui/icon "arrow-up")
  163. false
  164. (ui/icon "arrow-down")
  165. nil))))
  166. (defn- timestamp-cell-cp
  167. [_table row column]
  168. (some-> (get row (:id column))
  169. date/int->local-time-2))
  170. (defn- get-property-value-content
  171. [entity]
  172. (db-view/get-property-value-content (db/get-db) entity))
  173. (rum/defc block-container
  174. [config row]
  175. (let [container (state/get-component :block/container)
  176. config' (cond-> config
  177. (not (:popup? config))
  178. (assoc :view? true))]
  179. [:div.relative.w-full {:style {:min-height 24}}
  180. (if row
  181. (container config' row)
  182. [:div])]))
  183. (rum/defc block-title
  184. "Used on table view"
  185. [block {:keys [create-new-block property-ident width sidebar?]}]
  186. (let [inline-title (state/get-component :block/inline-title)]
  187. [:div.table-block-title.flex.items-center.w-full.h-full.cursor-pointer.items-center
  188. {:on-click (fn [e]
  189. (p/let [block (or block (and (fn? create-new-block) (create-new-block)))]
  190. (when block
  191. (cond
  192. (and (= property-ident :block/title) sidebar?)
  193. (route-handler/redirect-to-page! (:block/uuid block))
  194. (= property-ident :block/title)
  195. (let [selection-type (some-> (js/window.getSelection)
  196. (gobj/get "type"))]
  197. (when-not (= selection-type "Range")
  198. (state/sidebar-add-block! (state/get-current-repo) (:db/id block) :block)))
  199. :else
  200. (p/do!
  201. (shui/popup-show!
  202. (.-target e)
  203. (fn []
  204. [:div {:style {:min-width (max 160 width)}}
  205. (block-container {:popup? true} block)])
  206. {:align :start})
  207. (editor-handler/edit-block! block :max {:container-id :unknown-container}))))))}
  208. (if block
  209. [:div (inline-title
  210. (some->> (:block/title block)
  211. string/trim
  212. string/split-lines
  213. first))]
  214. [:div])]))
  215. (defn build-columns
  216. [config properties & {:keys [with-object-name? with-id? add-tags-column?]
  217. :or {with-object-name? true
  218. with-id? true
  219. add-tags-column? true}}]
  220. (let [;; FIXME: Shouldn't file graphs have :block/tags?
  221. add-tags-column?' (and (config/db-based-graph? (state/get-current-repo)) add-tags-column?)
  222. properties' (->>
  223. (if (or (some #(= (:db/ident %) :block/tags) properties) (not add-tags-column?'))
  224. properties
  225. (conj properties (db/entity :block/tags)))
  226. (remove nil?))]
  227. (->> (concat
  228. [{:id :select
  229. :name "Select"
  230. :header (fn [table _column] (header-checkbox table))
  231. :cell (fn [table row column]
  232. (row-checkbox table row column))
  233. :column-list? false
  234. :resizable? false}
  235. (when with-id?
  236. {:id :id
  237. :name "ID"
  238. :header (fn [_table _column] (header-index))
  239. :cell (fn [table row _column]
  240. (inc (.indexOf (:rows table) (:db/id row))))
  241. :resizable? false})
  242. (when with-object-name?
  243. {:id :block/title
  244. :name "Name"
  245. :type :string
  246. :header header-cp
  247. :cell (fn [_table row _column]
  248. (block-title row {:property-ident :block/title :sidebar? (:sidebar? config)}))
  249. :disable-hide? true})]
  250. (keep
  251. (fn [property]
  252. (when-let [ident (or (:db/ident property) (:id property))]
  253. ;; Hide properties that shouldn't ever be editable or that do not display well in a table
  254. (when-not (or (contains? #{:logseq.property/built-in? :logseq.property.asset/checksum :logseq.property.class/properties
  255. :block/created-at :block/updated-at :block/order :block/collapsed?
  256. :logseq.property/created-from-property}
  257. ident)
  258. (and with-object-name? (= :block/title ident))
  259. (contains? #{:map :entity} (:logseq.property/type property)))
  260. (let [property (if (de/entity? property)
  261. property
  262. (or (merge (db/entity ident) property) property)) ; otherwise, :cell/:header/etc. will be removed
  263. get-value (when (de/entity? property)
  264. (fn [row] (db-view/get-property-value-for-search row property)))]
  265. {:id ident
  266. :name (or (:name property)
  267. (:block/title property))
  268. :header (or (:header property)
  269. header-cp)
  270. :cell (or (:cell property)
  271. (when (de/entity? property)
  272. (fn [_table row _column style]
  273. (pv/property-value row property {:view? true
  274. :table-view? true
  275. :table-text-property-render
  276. (fn [block opts]
  277. (block-title block (assoc opts
  278. :width (:width style)
  279. :sidebar? (:sidebar? config))))}))))
  280. :get-value get-value
  281. :type (:type property)}))))
  282. properties')
  283. [{:id :block/created-at
  284. :name (t :page/created-at)
  285. :type :datetime
  286. :header header-cp
  287. :cell timestamp-cell-cp}
  288. {:id :block/updated-at
  289. :name (t :page/updated-at)
  290. :type :datetime
  291. :header header-cp
  292. :cell timestamp-cell-cp}])
  293. (remove nil?))))
  294. (defn- sort-columns
  295. [columns ordered-column-ids]
  296. (if (seq ordered-column-ids)
  297. (let [id->columns (zipmap (map :id columns) columns)
  298. ordered-id-set (set ordered-column-ids)]
  299. (concat
  300. (keep (fn [id]
  301. (get id->columns id))
  302. ordered-column-ids)
  303. (remove
  304. (fn [column] (ordered-id-set (:id column)))
  305. columns)))
  306. columns))
  307. (rum/defc more-actions
  308. [view-entity columns {:keys [column-visible? rows column-toggle-visibility]}]
  309. (let [display-type (:db/ident (:logseq.property.view/type view-entity))
  310. table? (= display-type :logseq.property.view/type.table)
  311. group-by-columns (concat (filter (fn [column]
  312. (when (:id column)
  313. (when-let [p (db/entity (:id column))]
  314. (and (not (db-property/many? p))
  315. (contains? #{:default :number :checkbox :url :node :date}
  316. (:logseq.property/type p)))))) columns)
  317. (when (contains? #{:linked-references :unlinked-references}
  318. (:logseq.property.view/feature-type view-entity))
  319. [{:id :block/page
  320. :name "Block Page"}]))]
  321. (shui/dropdown-menu
  322. (shui/dropdown-menu-trigger
  323. {:asChild true}
  324. (shui/button
  325. {:variant "ghost"
  326. :class "text-muted-foreground !px-1"
  327. :size :sm}
  328. (ui/icon "dots" {:size 15})))
  329. (shui/dropdown-menu-content
  330. {:align "end"}
  331. (shui/dropdown-menu-group
  332. (when table?
  333. (shui/dropdown-menu-sub
  334. (shui/dropdown-menu-sub-trigger
  335. "Columns visibility")
  336. (shui/dropdown-menu-sub-content
  337. (for [column (remove #(or (false? (:column-list? %))
  338. (:disable-hide? %)) columns)]
  339. (shui/dropdown-menu-checkbox-item
  340. {:key (str (:id column))
  341. :className "capitalize"
  342. :checked (column-visible? column)
  343. :onCheckedChange #(column-toggle-visibility column %)
  344. :onSelect (fn [e] (.preventDefault e))}
  345. (:name column))))))
  346. (when (seq group-by-columns)
  347. (shui/dropdown-menu-sub
  348. (shui/dropdown-menu-sub-trigger
  349. "Group by")
  350. (shui/dropdown-menu-sub-content
  351. (for [column group-by-columns]
  352. (shui/dropdown-menu-checkbox-item
  353. {:key (str (:id column))
  354. :className "capitalize"
  355. :checked (= (:id column) (:db/ident (:logseq.property.view/group-by-property view-entity)))
  356. :onCheckedChange (fn [result]
  357. (if result
  358. (db-property-handler/set-block-property! (:db/id view-entity) :logseq.property.view/group-by-property
  359. (:db/id (db/entity (:id column))))
  360. (db-property-handler/remove-block-property! (:db/id view-entity) :logseq.property.view/group-by-property)))
  361. :onSelect (fn [e] (.preventDefault e))}
  362. (:name column))))))
  363. (shui/dropdown-menu-item
  364. {:key "export-edn"
  365. :on-click #(db-export-handler/export-view-nodes-data rows)}
  366. "Export EDN"))))))
  367. (defn- get-column-size
  368. [column sized-columns]
  369. (let [id (:id column)
  370. size (get sized-columns id)]
  371. (cond
  372. (= id :id)
  373. 48
  374. (number? size)
  375. size
  376. (= id :logseq.property/query)
  377. 400
  378. :else
  379. (case id
  380. :select 32
  381. :add-property 160
  382. (:block/title :block/name) 360
  383. (:block/created-at :block/updated-at) 160
  384. 180))))
  385. (rum/defc add-property-button < rum/static
  386. []
  387. [:div.ls-table-header-cell.!border-0
  388. (shui/button
  389. {:variant "text"
  390. :class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start"}
  391. (ui/icon "plus")
  392. "New property")])
  393. (rum/defc action-bar < rum/static
  394. [table selected-rows {:keys [on-delete-rows]}]
  395. (shui/table-actions
  396. {}
  397. [:div (str (count selected-rows) " selected")]
  398. (selection/action-bar
  399. {:on-cut #(on-delete-rows table selected-rows)
  400. :selected-blocks selected-rows
  401. :hide-dots? true
  402. :button-border? true})))
  403. (rum/defc column-resizer
  404. [_column on-sized!]
  405. (let [*el (rum/use-ref nil)
  406. [dx set-dx!] (rum/use-state nil)
  407. [width set-width!] (rum/use-state nil)
  408. add-resizing-class #(dom/add-class! js/document.documentElement "is-resizing-buf")
  409. remove-resizing-class #(dom/remove-class! js/document.documentElement "is-resizing-buf")]
  410. (hooks/use-effect!
  411. (fn []
  412. (when (number? dx)
  413. (some-> (rum/deref *el)
  414. (dom/set-style! :transform (str "translate3D(" dx "px , 0, 0)")))))
  415. [dx])
  416. (hooks/use-effect!
  417. (fn []
  418. (when-let [el (and (fn? js/window.interact) (rum/deref *el))]
  419. (let [*field-rect (atom nil)
  420. min-width 40
  421. max-width 500]
  422. (-> (js/interact el)
  423. (.draggable
  424. (bean/->js
  425. {:listeners
  426. {:start (fn []
  427. (let [{:keys [width right] :as rect} (bean/->clj (.toJSON (.getBoundingClientRect (.closest el ".ls-table-header-cell"))))
  428. left-dx (if (>= width min-width) (- min-width width) 0)
  429. right-dx (if (<= width max-width) (- max-width width) 0)]
  430. (reset! *field-rect rect)
  431. (swap! *field-rect assoc
  432. ;; calculate left/right boundary
  433. :left-dx left-dx
  434. :right-dx right-dx
  435. :left-b (inc (+ left-dx right))
  436. :right-b (inc (+ right-dx right)))
  437. (dom/add-class! el "is-active")))
  438. :move (fn [^js e]
  439. (let [dx (.-dx e)
  440. pointer-x (js/Math.floor (.-clientX e))
  441. {:keys [left-b right-b]} @*field-rect
  442. left-b (js/Math.floor left-b)
  443. right-b (js/Math.floor right-b)]
  444. (when (and (> pointer-x left-b)
  445. (< pointer-x right-b))
  446. (set-dx! (fn [dx']
  447. (if (contains? #{min-width max-width} (abs dx'))
  448. dx'
  449. (let [to-dx (+ (or dx' 0) dx)
  450. {:keys [left-dx right-dx]} @*field-rect]
  451. (cond
  452. ;; left
  453. (neg? to-dx) (if (> (abs left-dx) (abs to-dx)) to-dx left-dx)
  454. ;; right
  455. (pos? to-dx) (if (> right-dx to-dx) to-dx right-dx)))))))))
  456. :end (fn []
  457. (set-dx!
  458. (fn [dx]
  459. (let [w (js/Math.round (+ dx (:width @*field-rect)))]
  460. (set-width! (cond
  461. (< w min-width) min-width
  462. (> w max-width) max-width
  463. :else w)))
  464. (reset! *field-rect nil)
  465. (dom/remove-class! el "is-active")
  466. 0)))}}))
  467. (.styleCursor false)
  468. (.on "dragstart" add-resizing-class)
  469. (.on "dragend" remove-resizing-class)
  470. (.on "mousedown" util/stop-propagation)))))
  471. [])
  472. (hooks/use-effect!
  473. (fn []
  474. (when (number? width)
  475. (on-sized! width)))
  476. [width])
  477. [:a.ls-table-resize-handle
  478. {:data-no-dnd true
  479. :ref *el}]))
  480. (defn- table-header-cell
  481. [table column]
  482. (let [header-fn (:header column)
  483. sized-columns (get-in table [:state :sized-columns])
  484. set-sized-columns! (get-in table [:data-fns :set-sized-columns!])
  485. width (get-column-size column sized-columns)
  486. select? (= :select (:id column))]
  487. [:div.ls-table-header-cell
  488. {:style {:width width
  489. :min-width width}
  490. :class (when select? "!border-0")}
  491. (if (fn? header-fn)
  492. (header-fn table column)
  493. header-fn)
  494. ;; resize handle
  495. (when-not (false? (:resizable? column))
  496. (column-resizer column
  497. (fn [size]
  498. (set-sized-columns! (assoc sized-columns (:id column) size)))))]))
  499. (defn- on-delete-rows
  500. [view-parent view-feature-type table selected-ids]
  501. (let [selected-rows (->> (map db/entity selected-ids)
  502. (remove :logseq.property/built-in?))
  503. pages (filter ldb/page? selected-rows)
  504. blocks (remove ldb/page? selected-rows)
  505. page-ids (map :db/id pages)
  506. {:keys [set-data! set-row-selection!]} (:data-fns table)
  507. update-table-state! (fn []
  508. (let [data (:full-data table)
  509. selected-ids (set (map :db/id selected-rows))
  510. new-data (if (every? number? data)
  511. (remove selected-ids data)
  512. ;; group
  513. (map (fn [[by-value col]]
  514. [by-value (remove selected-ids col)]) data))]
  515. (set-data! new-data)
  516. (set-row-selection! {})))]
  517. (p/do!
  518. (ui-outliner-tx/transact!
  519. {:outliner-op :delete-blocks}
  520. (when (seq blocks)
  521. (outliner-op/delete-blocks! blocks nil))
  522. (case view-feature-type
  523. :class-objects
  524. (when (seq page-ids)
  525. (let [tx-data (map (fn [pid] [:db/retract pid :block/tags (:db/id view-parent)]) page-ids)]
  526. (when (seq tx-data)
  527. (outliner-op/transact! tx-data {:outliner-op :save-block}))))
  528. :property-objects
  529. ;; Relationships with built-in properties must not be deleted e.g. built-in? or parent
  530. (when-not (:logseq.property/built-in? view-parent)
  531. (let [tx-data (map (fn [pid] [:db/retract pid (:db/ident view-parent)]) page-ids)]
  532. (when (seq tx-data)
  533. (outliner-op/transact! tx-data {:outliner-op :save-block}))))
  534. :query-result
  535. (doseq [page pages]
  536. (when-let [id (:block/uuid page)]
  537. (outliner-op/delete-page! id)))
  538. :all-pages
  539. (state/pub-event! [:page/show-delete-dialog selected-rows update-table-state!])
  540. nil))
  541. (when-not (or (= view-feature-type :all-pages)
  542. (and (= view-feature-type :property-objects) (:logseq.property/built-in? view-parent)))
  543. (update-table-state!)))))
  544. (defn- table-header
  545. [table {:keys [show-add-property? add-property! view-parent view-feature-type] :as option} selected-rows]
  546. (let [set-ordered-columns! (get-in table [:data-fns :set-ordered-columns!])
  547. pinned (get-in table [:state :pinned-columns])
  548. unpinned (get-in table [:state :unpinned-columns])
  549. build-item (fn [column]
  550. {:id (:name column)
  551. :value (:id column)
  552. :content (table-header-cell table column)
  553. :disabled? (= (:id column) :select)})
  554. pinned-items (mapv build-item pinned)
  555. unpinned-items (if show-add-property?
  556. (conj (mapv build-item unpinned)
  557. {:id "add property"
  558. :prop {:style {:width "-webkit-fill-available"
  559. :min-width 160}
  560. :on-click (fn [] (when (fn? add-property!) (add-property!)))}
  561. :value :add-new-property
  562. :content (add-property-button)
  563. :disabled? true})
  564. (mapv build-item unpinned))
  565. selection-rows-count (count selected-rows)]
  566. (shui/table-header
  567. (when (seq pinned-items)
  568. [:div.sticky-columns.flex.flex-row
  569. (dnd/items pinned-items {:vertical? false
  570. :on-drag-end (fn [ordered-columns _m]
  571. (set-ordered-columns! ordered-columns))})])
  572. (when (seq unpinned-items)
  573. [:div.flex.flex-row
  574. (dnd/items unpinned-items
  575. {:vertical? false
  576. :on-drag-end (fn [ordered-columns _m]
  577. (set-ordered-columns! ordered-columns))})])
  578. (when (pos? selection-rows-count)
  579. [:div.table-action-bar.absolute.top-0.left-8
  580. (action-bar table selected-rows
  581. (assoc option
  582. :on-delete-rows (fn [table selected-ids]
  583. (on-delete-rows view-parent view-feature-type table selected-ids))))]))))
  584. (rum/defc lazy-table-cell
  585. [cell-render-f cell-placeholder]
  586. (let [^js state (ui/useInView #js {:rootMargin "0px"})
  587. in-view? (.-inView state)]
  588. [:div.h-full {:ref (.-ref state)}
  589. (if in-view?
  590. (cell-render-f)
  591. cell-placeholder)]))
  592. (rum/defc table-row-inner < rum/static
  593. [{:keys [row-selected?] :as table} row props {:keys [show-add-property? scrolling?]}]
  594. (let [pinned-columns (get-in table [:state :pinned-columns])
  595. unpinned (get-in table [:state :unpinned-columns])
  596. unpinned-columns (if show-add-property?
  597. (conj (vec unpinned)
  598. {:id :add-property
  599. :cell (fn [_table _row _column])})
  600. unpinned)
  601. sized-columns (get-in table [:state :sized-columns])
  602. row-cell-f (fn [column {:keys [_lazy?]}]
  603. (let [id (str (:id row) "-" (:id column))
  604. width (get-column-size column sized-columns)
  605. select? (= (:id column) :select)
  606. add-property? (= (:id column) :add-property)
  607. style {:width width :min-width width}
  608. cell-opts {:key id
  609. :select? select?
  610. :add-property? add-property?
  611. :style style}
  612. cell-placeholder (shui/table-cell cell-opts nil)]
  613. (if (and scrolling? (not (:block/title row)))
  614. cell-placeholder
  615. (when-let [render (get column :cell)]
  616. (lazy-table-cell
  617. (fn [] (shui/table-cell cell-opts (render table row column style)))
  618. cell-placeholder)))))]
  619. (shui/table-row
  620. (merge
  621. props
  622. {:key (str (:db/id row))
  623. :data-state (when (row-selected? row) "selected")
  624. :on-pointer-down (fn [_e] (db-async/<get-block (state/get-current-repo) (:db/id row) {:children? false}))})
  625. (when (seq pinned-columns)
  626. [:div.sticky-columns.flex.flex-row
  627. (map #(row-cell-f % {}) pinned-columns)])
  628. (when (seq unpinned-columns)
  629. [:div.flex.flex-row
  630. (map #(row-cell-f % {:lazy? true}) unpinned-columns)]))))
  631. (rum/defc table-row < rum/reactive db-mixins/query
  632. [table row props option]
  633. (let [block (db/sub-block (:db/id row))
  634. row' (if (:block.temp/fully-loaded? block)
  635. (assoc block :block.temp/refs-count (:block.temp/refs-count row))
  636. row)]
  637. (table-row-inner table row' props option)))
  638. (rum/defc search
  639. [input {:keys [on-change set-input!]}]
  640. (let [[show-input? set-show-input!] (rum/use-state false)]
  641. (if show-input?
  642. [:div.flex.flex-row.items-center
  643. (shui/input
  644. {:placeholder "Type to search"
  645. :auto-focus true
  646. :value input
  647. :on-change (fn [e]
  648. (let [value (util/evalue e)]
  649. (on-change value)))
  650. :on-key-down (fn [e]
  651. (when (= "Escape" (util/ekey e))
  652. (set-show-input! false)
  653. (set-input! "")))
  654. :class "max-w-sm !h-7 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"})
  655. (shui/button
  656. {:variant "ghost"
  657. :class "text-muted-foreground !px-1"
  658. :size :sm
  659. :on-click #(do
  660. (set-show-input! false)
  661. (set-input! ""))}
  662. (ui/icon "x"))]
  663. (shui/button
  664. {:variant "ghost"
  665. ;; FIXME: remove ring when focused
  666. :class "text-muted-foreground !px-1"
  667. :size :sm
  668. :on-click #(set-show-input! true)}
  669. (ui/icon "search" {:size 15})))))
  670. (defn datetime-property?
  671. [property]
  672. (or
  673. (= :datetime (:logseq.property/type property))
  674. (contains? #{:block/created-at :block/updated-at} (:db/ident property))))
  675. (def timestamp-options
  676. [{:value "1 day ago"
  677. :label "1 day ago"}
  678. {:value "3 days ago"
  679. :label "3 days ago"}
  680. {:value "1 week ago"
  681. :label "1 week ago"}
  682. {:value "1 month ago"
  683. :label "1 month ago"}
  684. {:value "3 months ago"
  685. :label "3 months ago"}
  686. {:value "1 year ago"
  687. :label "1 year ago"}
  688. {:value "Custom date"
  689. :label "Custom date"}])
  690. (rum/defc filter-property < rum/static
  691. [view-entity columns {:keys [data-fns] :as table} opts]
  692. (let [[property set-property!] (rum/use-state nil)
  693. [values set-values!] (rum/use-state nil)
  694. schema (:schema (db/get-db))
  695. timestamp? (datetime-property? property)
  696. set-filters! (:set-filters! data-fns)
  697. filters (get-in table [:state :filters])
  698. columns (remove #(or (false? (:column-list? %))
  699. (= :id (:id %))) columns)
  700. items (map (fn [column]
  701. {:label (:name column)
  702. :value column}) columns)
  703. option {:input-default-placeholder "Filter"
  704. :input-opts {:class "!px-2 !py-1"}
  705. :items items
  706. :extract-fn :label
  707. :extract-chosen-fn :value
  708. :on-chosen (fn [column]
  709. (let [id (:id column)
  710. property (db/entity id)
  711. internal-property {:db/ident (:id column)
  712. :block/title (:name column)
  713. :logseq.property/type (:type column)}]
  714. (if (or property
  715. (= :db.cardinality/many (:db/cardinality (get schema id)))
  716. (not= (:type column) :string))
  717. (set-property! (or property internal-property))
  718. (do
  719. (shui/popup-hide!)
  720. (let [property internal-property
  721. new-filter [(:db/ident property) :text-contains]
  722. filters' (if (seq (:filters filters))
  723. (conj (:filters filters) new-filter)
  724. [new-filter])]
  725. (set-filters! {:or? (:or? filters)
  726. :filters filters'}))))))}
  727. checkbox? (= :checkbox (:logseq.property/type property))
  728. property-ident (:db/ident property)]
  729. (hooks/use-effect!
  730. (fn []
  731. (when (and view-entity property-ident (not (or timestamp? checkbox?)))
  732. (p/let [data (db-async/<get-property-values property-ident {:view-id (:db/id view-entity)
  733. :query-entity-ids (:query-entity-ids opts)})]
  734. (set-values! data))))
  735. [property-ident])
  736. (let [option (cond
  737. timestamp?
  738. (merge option
  739. {:items timestamp-options
  740. :input-default-placeholder (if property (:block/title property) "Select")
  741. :on-chosen (fn [value _ _ e]
  742. (shui/popup-hide!)
  743. (let [set-filter-fn (fn [value]
  744. (let [filters' (conj (:filters filters) [(:db/ident property) :after value])]
  745. (set-filters! {:or? (:or? filters)
  746. :filters filters'})))]
  747. (if (= value "Custom date")
  748. (shui/popup-show!
  749. (.-target e)
  750. (ui/nlp-calendar
  751. {:initial-focus true
  752. :datetime? false
  753. :on-day-click (fn [value]
  754. (set-filter-fn value)
  755. (shui/popup-hide!))})
  756. {})
  757. (set-filter-fn value))))})
  758. property
  759. (if checkbox?
  760. (let [items [{:value true :label "true"}
  761. {:value false :label "false"}]]
  762. (merge option
  763. {:items items
  764. :input-default-placeholder (if property (:block/title property) "Select")
  765. :on-chosen (fn [value]
  766. (let [filters' (conj (:filters filters) [(:db/ident property) :is value])]
  767. (set-filters! {:or? (:or? filters)
  768. :filters filters'})))}))
  769. (let [items values]
  770. (merge option
  771. {:items items
  772. :input-default-placeholder (if property (:block/title property) "Select")
  773. :multiple-choices? true
  774. :on-chosen (fn [_value _selected? selected]
  775. (let [selected-value (if (and (map? (first selected))
  776. (:block/uuid (first selected)))
  777. (set (map :block/uuid selected))
  778. selected)
  779. filters' (if (seq selected)
  780. (conj (:filters filters) [(:db/ident property) :is selected-value])
  781. (:filters filters))]
  782. (set-filters! {:or? (:or? filters)
  783. :filters filters'})))})))
  784. :else
  785. option)]
  786. (select/select option))))
  787. (rum/defc filter-properties < rum/static
  788. [view-entity columns table opts]
  789. (shui/button
  790. {:variant "ghost"
  791. :class "text-muted-foreground !px-1"
  792. :size :sm
  793. :on-click (fn [e]
  794. (shui/popup-show! (.-target e)
  795. (fn []
  796. (filter-property view-entity columns table opts))
  797. {:align :end
  798. :auto-focus? true}))}
  799. (ui/icon "filter")))
  800. (defn operator->text
  801. [operator]
  802. (case operator
  803. :is "is"
  804. :is-not "is not"
  805. :text-contains "text contains"
  806. :text-not-contains "text not contains"
  807. :date-before "date before"
  808. :date-after "date after"
  809. :before "before"
  810. :after "after"
  811. :number-gt ">"
  812. :number-lt "<"
  813. :number-gte ">="
  814. :number-lte "<="
  815. :between "between"))
  816. (defn get-property-operators
  817. [property]
  818. (if (datetime-property? property)
  819. [:before :after]
  820. (concat
  821. [:is :is-not]
  822. (case (:logseq.property/type property)
  823. (:default :url :node)
  824. [:text-contains :text-not-contains]
  825. (:date)
  826. [:date-before :date-after]
  827. :number
  828. [:number-gt :number-lt :number-gte :number-lte :between]
  829. nil))))
  830. (defn- get-filter-with-changed-operator
  831. [_property operator value]
  832. (case operator
  833. (:is :is-not)
  834. (when (set? value) value)
  835. (:text-contains :text-not-contains)
  836. (when (string? value) value)
  837. (:number-gt :number-lt :number-gte :number-lte)
  838. (when (number? value) value)
  839. :between
  840. (when (and (vector? value) (every? number? value))
  841. value)
  842. (:date-before :date-after :before :after)
  843. ;; FIXME: should be a valid date number
  844. (when (number? value) value)))
  845. (rum/defc filter-operator < rum/static
  846. [property operator filters set-filters! idx]
  847. (shui/dropdown-menu
  848. (shui/dropdown-menu-trigger
  849. {:asChild true}
  850. (shui/button
  851. {:class "!px-2 rounded-none border-r"
  852. :variant "ghost"
  853. :size :sm}
  854. [:span.text-xs (operator->text operator)]))
  855. (shui/dropdown-menu-content
  856. {:align "start"}
  857. (let [operators (get-property-operators property)]
  858. (for [operator operators]
  859. (shui/dropdown-menu-item
  860. {:on-click (fn []
  861. (let [new-filters (update filters :filters
  862. (fn [col]
  863. (update col idx
  864. (fn [[property _old-operator value]]
  865. (let [value' (get-filter-with-changed-operator property operator value)]
  866. (if value'
  867. [property operator value']
  868. [property operator]))))))]
  869. (set-filters! new-filters)))}
  870. (operator->text operator)))))))
  871. (rum/defc between < rum/static
  872. [_property [start end] filters set-filters! idx]
  873. [:<>
  874. (shui/input
  875. {:auto-focus true
  876. :placeholder "from"
  877. :value (str start)
  878. :onChange (fn [e]
  879. (let [input-value (util/evalue e)
  880. number-value (when-not (string/blank? input-value)
  881. (util/safe-parse-float input-value))
  882. value [number-value end]
  883. value (if (every? nil? value) nil value)]
  884. (let [new-filters (update filters :filters
  885. (fn [col]
  886. (update col idx
  887. (fn [[property operator _old_value]]
  888. (if (nil? value)
  889. [property operator]
  890. [property operator value])))))]
  891. (set-filters! new-filters))))
  892. :class "w-24 !h-6 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"})
  893. (shui/input
  894. {:value (str end)
  895. :placeholder "to"
  896. :onChange (fn [e]
  897. (let [input-value (util/evalue e)
  898. number-value (when-not (string/blank? input-value)
  899. (util/safe-parse-float input-value))
  900. value [start number-value]
  901. value (if (every? nil? value) nil value)]
  902. (let [new-filters (update filters :filters
  903. (fn [col]
  904. (update col idx
  905. (fn [[property operator _old_value]]
  906. (if (nil? value)
  907. [property operator]
  908. [property operator value])))))]
  909. (set-filters! new-filters))))
  910. :class "w-24 !h-6 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"})])
  911. (rum/defc ^:large-vars/cleanup-todo filter-value-select < rum/static
  912. [view-entity {:keys [data-fns] :as table} property value operator idx opts]
  913. (let [type (:logseq.property/type property)
  914. property-ident (:db/ident property)
  915. [values set-values!] (hooks/use-state nil)
  916. [dropdown-open? set-dropdown-open!] (hooks/use-state false)]
  917. (hooks/use-effect!
  918. (fn []
  919. (p/do!
  920. (let [values (if (coll? value) value [value])
  921. ids (filter #(and (uuid? %) (nil? (db/entity [:block/uuid %]))) values)]
  922. (when (seq ids) (db-async/<get-blocks (state/get-current-repo) ids)))
  923. (when (and property-ident dropdown-open?
  924. (not (contains? #{:data :datetime :checkbox} type)))
  925. (p/let [data (db-async/<get-property-values property-ident {:view-id (:db/id view-entity)
  926. :query-entity-ids (:query-entity-ids opts)})]
  927. (set-values! (map (fn [v] (if (map? (:value v))
  928. (assoc v :value (:block/uuid (:value v)))
  929. v)) data))))))
  930. [property-ident dropdown-open?])
  931. (let [items (cond
  932. (contains? #{:before :after} operator)
  933. timestamp-options
  934. (= type :checkbox)
  935. [{:value true :label "true"} {:value false :label "false"}]
  936. :else
  937. values)
  938. filters (get-in table [:state :filters])
  939. set-filters! (:set-filters! data-fns)
  940. many? (if (or (contains? #{:date-before :date-after :before :after} operator)
  941. (contains? #{:checkbox} type))
  942. false
  943. true)
  944. option (cond->
  945. {:input-default-placeholder (:block/title property)
  946. :input-opts {:class "!px-3 !py-1"}
  947. :items items
  948. :extract-fn :label
  949. :extract-chosen-fn :value
  950. :on-chosen (fn [value _selected? selected e]
  951. (when-not many?
  952. (shui/popup-hide!))
  953. (let [value' (if many? selected value)
  954. set-filters-fn (fn [value']
  955. (let [new-filters (update filters :filters
  956. (fn [col]
  957. (update col idx
  958. (fn [[property operator _value]]
  959. [property operator value']))))]
  960. (set-filters! new-filters)))]
  961. (if (= value "Custom date")
  962. (shui/popup-show!
  963. (.-target e)
  964. (ui/nlp-calendar
  965. {:initial-focus true
  966. :datetime? false
  967. :on-day-click (fn [value]
  968. (set-filters-fn value)
  969. (shui/popup-hide!))})
  970. {})
  971. (set-filters-fn value'))))}
  972. many?
  973. (assoc
  974. :multiple-choices? true
  975. :selected-choices value))]
  976. (shui/dropdown-menu
  977. (shui/dropdown-menu-trigger
  978. {:asChild true}
  979. (shui/button
  980. {:class "!px-2 rounded-none border-r"
  981. :variant "ghost"
  982. :size :sm
  983. :on-click #(set-dropdown-open! (not dropdown-open?))}
  984. (let [value (cond
  985. (uuid? value)
  986. (db/entity [:block/uuid value])
  987. (instance? js/Date value)
  988. (some->> (tc/to-date value)
  989. (t/to-default-time-zone)
  990. (tf/unparse (tf/formatter "yyyy-MM-dd")))
  991. (and (coll? value) (every? uuid? value))
  992. (keep #(db/entity [:block/uuid %]) value)
  993. :else
  994. value)]
  995. [:div.flex.flex-row.items-center.gap-1.text-xs
  996. (cond
  997. (de/entity? value)
  998. [:div (get-property-value-content value)]
  999. (string? value)
  1000. [:div value]
  1001. (boolean? value)
  1002. [:div (str value)]
  1003. (seq value)
  1004. (->> (map (fn [v] [:div (get-property-value-content v)]) value)
  1005. (interpose [:div "or"]))
  1006. :else
  1007. "All")])))
  1008. (shui/dropdown-menu-content
  1009. {:align "start"}
  1010. (select/select option))))))
  1011. (rum/defc filter-value < rum/static
  1012. [view-entity table property operator value filters set-filters! idx opts]
  1013. (let [number-operator? (string/starts-with? (name operator) "number-")]
  1014. (case operator
  1015. :between
  1016. (between property value filters set-filters! idx)
  1017. (:text-contains :text-not-contains :number-gt :number-lt :number-gte :number-lte)
  1018. (shui/input
  1019. {:auto-focus false
  1020. :value (or value "")
  1021. :onChange (fn [e]
  1022. (let [value (util/evalue e)
  1023. number-value (and number-operator? (when-not (string/blank? value)
  1024. (util/safe-parse-float value)))]
  1025. (let [new-filters (update filters :filters
  1026. (fn [col]
  1027. (update col idx
  1028. (fn [[property operator _value]]
  1029. (if (and number-operator? (nil? number-value))
  1030. [property operator]
  1031. [property operator (or number-value value)])))))]
  1032. (set-filters! new-filters))))
  1033. :class "w-24 !h-6 !py-0 border-none focus-visible:ring-0 focus-visible:ring-offset-0"})
  1034. (filter-value-select view-entity table property value operator idx opts))))
  1035. (rum/defc filters-row < rum/static ;
  1036. [view-entity {:keys [data-fns columns] :as table} opts]
  1037. (let [filters (get-in table [:state :filters])
  1038. {:keys [set-filters!]} data-fns]
  1039. (when (seq (:filters filters))
  1040. [:div.filters-row.flex.flex-row.items-center.gap-4.justify-between.flex-wrap.py-2
  1041. [:div.flex.flex-row.items-center.gap-2
  1042. (map-indexed
  1043. (fn [idx filter']
  1044. (let [[property-ident operator value] filter'
  1045. property (if (= property-ident :block/title)
  1046. {:db/ident property-ident
  1047. :block/title "Name"}
  1048. (or (db/entity property-ident)
  1049. (some (fn [column] (when (= (:id column) property-ident)
  1050. {:db/ident (:id column)
  1051. :block/title (:name column)})) columns)))]
  1052. [:div.flex.flex-row.items-center.border.rounded
  1053. (shui/button
  1054. {:class "!px-2 rounded-none border-r"
  1055. :variant "ghost"
  1056. :size :sm
  1057. :disabled true}
  1058. [:span.text-xs (:block/title property)])
  1059. (filter-operator property operator filters set-filters! idx)
  1060. (filter-value view-entity table property operator value filters set-filters! idx opts)
  1061. (shui/button
  1062. {:class "!px-1 rounded-none text-muted-foreground"
  1063. :variant "ghost"
  1064. :size :sm
  1065. :on-click (fn [_e]
  1066. (let [new-filters (update filters :filters (fn [col] (vec (remove #{filter'} col))))]
  1067. (set-filters! new-filters)))}
  1068. (ui/icon "x"))]))
  1069. (:filters filters))]
  1070. (when (> (count (:filters filters)) 1)
  1071. [:div
  1072. (shui/select
  1073. {:default-value (if (:or? filters) "or" "and")
  1074. :on-value-change (fn [v]
  1075. (set-filters! (assoc filters :or? (= v "or"))))}
  1076. (shui/select-trigger
  1077. {:class "opacity-75 hover:opacity-100 !px-2 !py-0 !h-6"}
  1078. (shui/select-value
  1079. {:placeholder "Match"}))
  1080. (shui/select-content
  1081. (shui/select-group
  1082. (shui/select-item {:value "and"} "Match all filters")
  1083. (shui/select-item {:value "or"} "Match any filter"))))])])))
  1084. (rum/defc new-record-button < rum/static
  1085. [table view-entity]
  1086. (let [asset? (and (:logseq.property/built-in? view-entity)
  1087. (= (:block/name view-entity) "asset"))]
  1088. (ui/tooltip
  1089. (shui/button
  1090. {:variant "ghost"
  1091. :class "!px-1 text-muted-foreground"
  1092. :size :sm
  1093. :on-click (fn [_]
  1094. (let [f (get-in table [:data-fns :add-new-object!])]
  1095. (f view-entity table)))}
  1096. (ui/icon (if asset? "upload" "plus")))
  1097. [:div "New node"])))
  1098. (rum/defc add-new-row < rum/static
  1099. [view-entity table]
  1100. [: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
  1101. {:on-click (fn [_]
  1102. (let [f (get-in table [:data-fns :add-new-object!])]
  1103. (f view-entity table)))}
  1104. (ui/icon "plus" {:size 14})
  1105. [:div "New"]])
  1106. (defn- table-filters->persist-state
  1107. [filters]
  1108. (mapv
  1109. (fn [[property operator matches]]
  1110. (let [matches' (cond
  1111. (de/entity? matches)
  1112. (:block/uuid matches)
  1113. (and (coll? matches) (every? de/entity? matches))
  1114. (set (map :block/uuid matches))
  1115. :else
  1116. matches)]
  1117. (if (some? matches')
  1118. [property operator matches']
  1119. [property operator])))
  1120. filters))
  1121. (defn- db-set-table-state!
  1122. [entity {:keys [set-sorting! set-filters! set-visible-columns!
  1123. set-ordered-columns! set-sized-columns!]}]
  1124. (let [repo (state/get-current-repo)
  1125. db-based? (config/db-based-graph?)]
  1126. {:set-sorting!
  1127. (fn [sorting]
  1128. (p/do!
  1129. (when db-based? (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/sorting sorting))
  1130. (set-sorting! sorting)))
  1131. :set-filters!
  1132. (fn [filters]
  1133. (let [filters (-> (update filters :filters table-filters->persist-state)
  1134. (update :or? boolean))]
  1135. (p/do!
  1136. (when db-based? (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/filters filters))
  1137. (set-filters! filters))))
  1138. :set-visible-columns!
  1139. (fn [columns]
  1140. (let [hidden-columns (vec (keep (fn [[column visible?]]
  1141. (when (false? visible?)
  1142. column)) columns))]
  1143. (p/do!
  1144. (when db-based? (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/hidden-columns hidden-columns))
  1145. (set-visible-columns! columns))))
  1146. :set-ordered-columns!
  1147. (fn [ordered-columns]
  1148. (let [ids (vec (remove #{:select} ordered-columns))]
  1149. (p/do!
  1150. (when db-based? (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/ordered-columns ids))
  1151. (set-ordered-columns! ordered-columns))))
  1152. :set-sized-columns!
  1153. (fn [sized-columns]
  1154. (p/do!
  1155. (when db-based? (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/sized-columns sized-columns))
  1156. (set-sized-columns! sized-columns)))}))
  1157. (rum/defc lazy-item
  1158. [data idx {:keys [properties list-view? scrolling?]} item-render]
  1159. (let [item (util/nth-safe data idx)
  1160. db-id (cond (map? item) (:db/id item)
  1161. (number? item) item
  1162. :else nil)
  1163. [item set-item!] (hooks/use-state nil)
  1164. opts (if list-view?
  1165. {:skip-refresh? true
  1166. :children? false}
  1167. {:children? false
  1168. :properties properties
  1169. :skip-transact? true
  1170. :skip-refresh? true})]
  1171. (hooks/use-effect!
  1172. #(c.m/run-task*
  1173. (m/sp
  1174. (when (and db-id (not item) (not scrolling?))
  1175. (let [block (c.m/<? (db-async/<get-block (state/get-current-repo) db-id opts))
  1176. block' (if list-view? (db/entity db-id) block)]
  1177. (set-item! block')))))
  1178. [db-id scrolling?])
  1179. (let [item' (cond (map? item) item (number? item) {:db/id item})]
  1180. (item-render item'))))
  1181. (rum/defc table-body < rum/static
  1182. [table option rows *scroller-ref set-items-rendered!]
  1183. (let [[scrolling? set-scrolling!] (hooks/use-state false)
  1184. sidebar? (get-in option [:config :sidebar?])]
  1185. (when (seq rows)
  1186. (ui/virtualized-list
  1187. {:ref #(reset! *scroller-ref %)
  1188. :custom-scroll-parent (if sidebar?
  1189. (first (dom/by-class "sidebar-item-list"))
  1190. (gdom/getElement "main-content-container"))
  1191. :compute-item-key (fn [idx]
  1192. (let [block-id (util/nth-safe rows idx)]
  1193. (str "table-row-" block-id)))
  1194. :skipAnimationFrameInResizeObserver true
  1195. :total-count (count rows)
  1196. :context {:scrolling scrolling?}
  1197. :is-scrolling set-scrolling!
  1198. :item-content (fn [idx _user ^js context]
  1199. (let [option (assoc option
  1200. :scrolling? (.-scrolling context)
  1201. :table-view? true)]
  1202. (lazy-item (:data table) idx option
  1203. (fn [row]
  1204. (table-row table row {} option)))))
  1205. :items-rendered (fn [props]
  1206. (when (seq props)
  1207. (set-items-rendered! true)))}))))
  1208. (rum/defc table-view < rum/static
  1209. [table option row-selection *scroller-ref]
  1210. (let [selected-rows (shui/table-get-selection-rows row-selection (:rows table))
  1211. [items-rendered? set-items-rendered!] (hooks/use-state false)]
  1212. (shui/table
  1213. (let [rows (:rows table)]
  1214. [:div.ls-table-rows.content.overflow-x-auto.force-visible-scrollbar
  1215. [:div.relative
  1216. (table-header table option selected-rows)
  1217. (table-body table option rows *scroller-ref set-items-rendered!)
  1218. (when (and (get-in table [:data-fns :add-new-object!]) (or (empty? rows) items-rendered?))
  1219. (shui/table-footer (add-new-row (:view-entity option) table)))]]))))
  1220. (rum/defc list-view < rum/static
  1221. [{:keys [config] :as option} _view-entity {:keys [rows]} *scroller-ref]
  1222. (let [lazy-item-render (fn [rows idx]
  1223. (lazy-item rows idx (assoc option :list-view? true)
  1224. (fn [block]
  1225. (block-container (assoc config :list-view? true) block))))
  1226. list-cp (fn [rows]
  1227. (when (seq rows)
  1228. (ui/virtualized-list
  1229. {:ref #(reset! *scroller-ref %)
  1230. :class "content"
  1231. :custom-scroll-parent (gdom/getElement "main-content-container")
  1232. :increase-viewport-by {:top 64 :bottom 64}
  1233. :compute-item-key (fn [idx]
  1234. (let [block-id (util/nth-safe rows idx)]
  1235. (str "list-row-" block-id)))
  1236. :total-count (count rows)
  1237. :skipAnimationFrameInResizeObserver true
  1238. :item-content (fn [idx] (lazy-item-render rows idx))})))
  1239. breadcrumb (state/get-component :block/breadcrumb)
  1240. all-numbers? (every? number? rows)]
  1241. (if all-numbers?
  1242. (list-cp rows)
  1243. (for [[idx row] (medley/indexed rows)]
  1244. (if (and (vector? row) (uuid? (first row)))
  1245. (let [[first-block-id blocks] row]
  1246. [:div
  1247. {:key (str "partition-" first-block-id)}
  1248. [:div.ml-6.text-sm.opacity-70.hover:opacity-100.mt-1
  1249. (breadcrumb (assoc config :list-view? true)
  1250. (state/get-current-repo) first-block-id
  1251. {:show-page? false})]
  1252. (list-cp blocks)])
  1253. (rum/with-key
  1254. (lazy-item-render rows idx)
  1255. (str "partition-" idx)))))))
  1256. (rum/defc gallery-card-item
  1257. [view-entity block config]
  1258. [:div.ls-card-item.content
  1259. {:key (str "view-card-" (:db/id view-entity) "-" (:db/id block))}
  1260. [:div.-ml-4
  1261. (block-container (assoc config
  1262. :id (str (:block/uuid block))
  1263. :gallery-view? true
  1264. :view? true)
  1265. block)]])
  1266. (rum/defcs gallery-view < rum/static mixins/container-id
  1267. [state {:keys [config]} table view-entity blocks *scroller-ref]
  1268. (let [config' (assoc config :container-id (:container-id state))]
  1269. [:div.ls-cards
  1270. (when (seq blocks)
  1271. (ui/virtualized-grid
  1272. {:ref #(reset! *scroller-ref %)
  1273. :total-count (count blocks)
  1274. :custom-scroll-parent (gdom/getElement "main-content-container")
  1275. :skipAnimationFrameInResizeObserver true
  1276. :compute-item-key (fn [idx]
  1277. (str (:db/id view-entity) "-card-" idx))
  1278. :item-content (fn [idx]
  1279. (lazy-item (:data table) idx {}
  1280. (fn [block]
  1281. (gallery-card-item view-entity block config'))))}))]))
  1282. (defn- run-effects!
  1283. [option {:keys [data]} *scroller-ref gallery?]
  1284. (hooks/use-effect!
  1285. (fn []
  1286. (when (and (:current-page? (:config option)) (seq data) (map? (first data)) (:block/uuid (first data)))
  1287. (ui-handler/scroll-to-anchor-block @*scroller-ref data gallery?)
  1288. (state/set-state! :editor/virtualized-scroll-fn #(ui-handler/scroll-to-anchor-block @*scroller-ref data gallery?))))
  1289. []))
  1290. (rum/defc view-sorting-item
  1291. [table sorting id name asc? set-sorting!]
  1292. [:div.flex.flex-row.gap-2.items-center.justify-between.px-2
  1293. [:div:div.flex.flex-row.gap-1.items-center
  1294. (shui/button
  1295. {:size :sm
  1296. :class "!px-1"
  1297. :variant :ghost
  1298. :title "Drag && Drop to reorder"}
  1299. (shui/tabler-icon "grip-vertical" {:size 14}))
  1300. [:div.text-muted-foreground.whitespace-nowrap (str name ":")]]
  1301. [:div.flex.flex-row.gap-2.items-center
  1302. (shui/select
  1303. {:default-value (if asc? "asc" "desc")
  1304. :on-value-change (fn [v]
  1305. (let [asc? (= v "asc")
  1306. f (:column-set-sorting! table)]
  1307. (when f
  1308. (f sorting {:id id} asc?))))}
  1309. (shui/select-trigger
  1310. {:class "order-button !px-2 !py-0 !h-8"}
  1311. (shui/select-value
  1312. {:placeholder "Select order"}))
  1313. (shui/select-content
  1314. (shui/select-group
  1315. (shui/select-item {:value "asc"} "Ascending")
  1316. (shui/select-item {:value "desc"} "Descending"))))
  1317. (shui/button
  1318. {:variant "ghost"
  1319. :class "text-muted-foreground !px-1"
  1320. :size :sm
  1321. :on-click (fn []
  1322. (let [f (:column-set-sorting! table)
  1323. new-sorting (f sorting {:id id} nil)
  1324. f (get-in table [:data-fns :set-sorting!])]
  1325. (set-sorting! new-sorting)
  1326. (f new-sorting)
  1327. (when (empty? new-sorting)
  1328. (shui/popup-hide!))))}
  1329. (ui/icon "x"))]])
  1330. (rum/defc view-sorting-config
  1331. [table sorting columns]
  1332. (let [[sorting set-sorting!] (rum/use-state sorting)]
  1333. [:div.ls-view-order-setting.flex.flex-col.gap-2.py-2.text-sm
  1334. (let [items (for [{:keys [id asc?]} sorting]
  1335. (when-let [name (some (fn [column] (when (= id (:id column))
  1336. (:name column))) columns)]
  1337. {:id (str id)
  1338. :value id
  1339. :content (view-sorting-item table sorting id name asc? set-sorting!)}))]
  1340. (dnd/items items
  1341. {:on-drag-end (fn [ordered-columns]
  1342. (let [f (get-in table [:data-fns :set-sorting!])
  1343. new-sorting (mapv (fn [column] (some #(when (= column (:id %)) %) sorting)) ordered-columns)]
  1344. (set-sorting! new-sorting)
  1345. (f new-sorting)))}))
  1346. (shui/dropdown-menu-item
  1347. {:class "text-muted-foreground pl-3"
  1348. :on-click (fn []
  1349. (let [f (get-in table [:data-fns :set-sorting!])]
  1350. (set-sorting! nil)
  1351. (f nil)
  1352. (shui/popup-hide!)))}
  1353. (ui/icon "trash" {:size 15})
  1354. [:span.ml-1 "Delete sort"])]))
  1355. (rum/defc view-sorting
  1356. [table columns sorting]
  1357. (shui/button
  1358. {:variant "ghost"
  1359. :class "text-muted-foreground !px-1"
  1360. :size :sm
  1361. :on-click (fn [e]
  1362. (shui/popup-show! (.-target e)
  1363. (fn [] (view-sorting-config table sorting columns))
  1364. {:align :end}))}
  1365. (ui/icon "arrows-up-down")))
  1366. (defn- view-cp
  1367. [view-entity table option* {:keys [*scroller-ref display-type row-selection]}]
  1368. (let [option (assoc option* :view-entity view-entity)]
  1369. (case display-type
  1370. :logseq.property.view/type.list
  1371. (list-view option view-entity table *scroller-ref)
  1372. :logseq.property.view/type.gallery
  1373. (gallery-view option table view-entity (:rows table) *scroller-ref)
  1374. (table-view table option row-selection *scroller-ref))))
  1375. (defn- get-views
  1376. [ent view-feature-type]
  1377. (let [entity (db/entity (:db/id ent))
  1378. views (->> (:logseq.property/_view-for entity)
  1379. (filter (fn [view]
  1380. (= view-feature-type (:logseq.property.view/feature-type view)))))]
  1381. (ldb/sort-by-order views)))
  1382. (defn- create-view!
  1383. [view-parent view-feature-type]
  1384. (when-let [page (db/get-case-page common-config/views-page-name)]
  1385. (p/let [properties (cond->
  1386. {:logseq.property/view-for (:db/id view-parent)
  1387. :logseq.property.view/feature-type view-feature-type}
  1388. (contains? #{:linked-references :unlinked-references} view-feature-type)
  1389. (assoc :logseq.property.view/type (:db/id (db/entity :logseq.property.view/type.list))
  1390. :logseq.property.view/group-by-property (:db/id (db/entity :block/page))))
  1391. view-exists? (seq (get-views view-parent view-feature-type))
  1392. view-title (if view-exists?
  1393. ""
  1394. (case view-feature-type
  1395. :linked-references
  1396. "Linked references"
  1397. :unlinked-references
  1398. "Unlinked references"
  1399. :class-objects
  1400. "All"
  1401. :property-objects
  1402. "All"
  1403. :all-pages
  1404. "All pages"
  1405. ""))
  1406. result (editor-handler/api-insert-new-block! view-title
  1407. {:page (:block/uuid page)
  1408. :properties properties
  1409. :edit-block? false})]
  1410. (db/entity [:block/uuid (:block/uuid result)]))))
  1411. (rum/defc views-tab < rum/reactive db-mixins/query
  1412. [view-parent current-view {:keys [views data items-count set-view-entity! set-data! set-views! view-feature-type show-items-count? references? opacity]}]
  1413. [:div.views
  1414. (for [view* views]
  1415. (let [view (db/sub-block (:db/id view*))
  1416. current-view? (= (:db/id current-view) (:db/id view))]
  1417. (shui/button
  1418. {:variant :text
  1419. :size :sm
  1420. :class (str "text-sm px-0 py-0 h-6 " (when-not current-view? "text-muted-foreground"))
  1421. :on-click (fn [e]
  1422. (if (and current-view? (not= (:db/id view) (:db/id view-parent)))
  1423. (shui/popup-show!
  1424. (.-target e)
  1425. (fn []
  1426. [:<>
  1427. (shui/dropdown-menu-sub
  1428. (shui/dropdown-menu-sub-trigger
  1429. "Rename")
  1430. (shui/dropdown-menu-sub-content
  1431. (when-let [block-container-cp (state/get-component :block/container)]
  1432. (block-container-cp {} view))))
  1433. (shui/dropdown-menu-item
  1434. {:key "Delete"
  1435. :on-click (fn []
  1436. (p/do!
  1437. (editor-handler/delete-block-aux! view)
  1438. (let [views' (remove (fn [v] (= (:db/id v) (:db/id view))) views)]
  1439. (set-views! views')
  1440. (set-view-entity! (first views'))
  1441. (shui/popup-hide!))))}
  1442. "Delete")])
  1443. {:as-dropdown? true
  1444. :align "start"
  1445. :content-props {:onClick shui/popup-hide!}})
  1446. (do
  1447. (set-view-entity! view)
  1448. (set-data! nil))))}
  1449. (when-not references?
  1450. (let [display-type (or (:db/ident (get view :logseq.property.view/type))
  1451. :logseq.property.view/type.table)]
  1452. (when-let [icon (:logseq.property/icon (db/entity display-type))]
  1453. (icon-component/icon icon {:color? true
  1454. :size 15}))))
  1455. (let [title (:block/title view)]
  1456. (if (= title "")
  1457. "New view"
  1458. title))
  1459. (when (and current-view? show-items-count? (> items-count 0) (seq data))
  1460. [:span.text-muted-foreground.text-xs
  1461. items-count]))))
  1462. (shui/button
  1463. {:variant :text
  1464. :size :sm
  1465. :title "Add new view"
  1466. :class (str "!px-1 -ml-1 text-muted-foreground hover:text-foreground transition-opacity ease-in duration-300 " opacity)
  1467. :on-click (fn []
  1468. (p/let [view (create-view! view-parent view-feature-type)]
  1469. (set-views! (concat views [view]))))}
  1470. (ui/icon "plus" {:size 15}))])
  1471. (rum/defc view-head < rum/static
  1472. [view-parent view-entity table columns input sorting
  1473. set-input! add-new-object!
  1474. {:keys [view-feature-type title-key additional-actions]
  1475. :as option}]
  1476. (let [[hover? set-hover?] (hooks/use-state nil)
  1477. db-based? (config/db-based-graph? (state/get-current-repo))
  1478. references? (contains? #{:linked-references :unlinked-references} view-feature-type)
  1479. opacity (cond
  1480. (and references? (not hover?)) "opacity-0"
  1481. hover? "opacity-100"
  1482. :else "opacity-75")]
  1483. [:div.flex.flex-1.flex-nowrap.items-center.justify-between.gap-1.overflow-hidden
  1484. {:on-mouse-over #(set-hover? true)
  1485. :on-mouse-out #(set-hover? false)}
  1486. [:div.flex.flex-row.items-center.gap-2
  1487. (if db-based?
  1488. (if (= view-feature-type :query-result)
  1489. [:div.font-medium.opacity-50.text-sm
  1490. (t (or title-key :views.table/default-title)
  1491. (count (:rows table)))]
  1492. (views-tab view-parent view-entity (assoc option
  1493. :hover? hover?
  1494. :opacity opacity
  1495. :references? references?)))
  1496. [:div.font-medium.text-sm
  1497. [:span
  1498. (case view-feature-type
  1499. :all-pages "All pages"
  1500. :linked-references "Linked references"
  1501. :unlinked-references "Unlinked references"
  1502. "Nodes")]
  1503. [:span.ml-1 (count (:rows table))]])]
  1504. [:div.view-actions.flex.items-center.gap-1.transition-opacity.ease-in.duration-300
  1505. {:class opacity}
  1506. (when (seq additional-actions)
  1507. [:<> (for [action additional-actions]
  1508. (if (fn? action)
  1509. (action option)
  1510. action))])
  1511. (when (and db-based? (seq sorting))
  1512. (view-sorting table columns sorting))
  1513. (when db-based? (filter-properties view-entity columns table option))
  1514. (search input {:on-change set-input!
  1515. :set-input! set-input!})
  1516. (when db-based?
  1517. [:div.text-muted-foreground.text-sm
  1518. (pv/property-value view-entity (db/entity :logseq.property.view/type) {})])
  1519. (when db-based? (more-actions view-entity columns table))
  1520. (when (and db-based? add-new-object!) (new-record-button table view-entity))]]))
  1521. (rum/defc ^:large-vars/cleanup-todo view-inner < rum/static
  1522. [view-entity {:keys [view-parent data full-data set-data! columns add-new-object! foldable-options input set-input! sorting set-sorting! filters set-filters! view-feature-type] :as option*}
  1523. *scroller-ref]
  1524. (let [db-based? (config/db-based-graph?)
  1525. option (assoc option* :properties
  1526. (-> (remove #{:id :select} (map :id columns))
  1527. (conj :block/uuid :block/name)
  1528. vec))
  1529. default-visible-columns (if-let [hidden-columns (conj (:logseq.property.table/hidden-columns view-entity) :id)]
  1530. (zipmap hidden-columns (repeat false))
  1531. ;; This case can happen for imported tables
  1532. (if (seq (:logseq.property.table/ordered-columns view-entity))
  1533. (zipmap (set/difference (set (map :id columns))
  1534. (set (:logseq.property.table/ordered-columns view-entity))
  1535. #{:select :block/created-at :block/updated-at})
  1536. (repeat false))
  1537. {}))
  1538. [visible-columns set-visible-columns!] (rum/use-state default-visible-columns)
  1539. ordered-columns (vec (concat [:select] (:logseq.property.table/ordered-columns view-entity)))
  1540. sized-columns (:logseq.property.table/sized-columns view-entity)
  1541. [ordered-columns set-ordered-columns!] (rum/use-state ordered-columns)
  1542. [sized-columns set-sized-columns!] (rum/use-state sized-columns)
  1543. {:keys [set-sorting! set-filters! set-visible-columns! set-ordered-columns! set-sized-columns!]}
  1544. (db-set-table-state! view-entity {:set-sorting! set-sorting!
  1545. :set-filters! set-filters!
  1546. :set-visible-columns! set-visible-columns!
  1547. :set-sized-columns! set-sized-columns!
  1548. :set-ordered-columns! set-ordered-columns!})
  1549. [row-selection set-row-selection!] (rum/use-state {})
  1550. columns (sort-columns columns ordered-columns)
  1551. select? (first (filter (fn [item] (= (:id item) :select)) columns))
  1552. id? (first (filter (fn [item] (= (:id item) :id)) columns))
  1553. pinned-properties (set (cond->> (map :db/ident (:logseq.property.table/pinned-columns view-entity))
  1554. id?
  1555. (cons :id)
  1556. select?
  1557. (cons :select)))
  1558. {pinned true unpinned false} (group-by (fn [item]
  1559. (contains? pinned-properties (:id item)))
  1560. (remove (fn [column]
  1561. (false? (get visible-columns (:id column))))
  1562. columns))
  1563. group-by-property (:logseq.property.view/group-by-property view-entity)
  1564. table-map {:view-entity view-entity
  1565. :data data
  1566. :full-data full-data
  1567. :columns columns
  1568. :state {:sorting sorting
  1569. :filters filters
  1570. :row-selection row-selection
  1571. :visible-columns visible-columns
  1572. :sized-columns sized-columns
  1573. :ordered-columns ordered-columns
  1574. :pinned-columns pinned
  1575. :unpinned-columns unpinned
  1576. :group-by-property group-by-property}
  1577. :data-fns {:set-data! set-data!
  1578. :set-filters! set-filters!
  1579. :set-sorting! set-sorting!
  1580. :set-visible-columns! set-visible-columns!
  1581. :set-ordered-columns! set-ordered-columns!
  1582. :set-sized-columns! set-sized-columns!
  1583. :set-row-selection! set-row-selection!
  1584. :add-new-object! add-new-object!}}
  1585. table (shui/table-option table-map)
  1586. *view-ref (rum/use-ref nil)
  1587. display-type (if (config/db-based-graph?)
  1588. (or (:db/ident (get view-entity :logseq.property.view/type))
  1589. (when (= (:view-type option) :linked-references)
  1590. :logseq.property.view/type.list)
  1591. :logseq.property.view/type.table)
  1592. (if (= view-feature-type :all-pages)
  1593. :logseq.property.view/type.table
  1594. :logseq.property.view/type.list))
  1595. gallery? (= display-type :logseq.property.view/type.gallery)
  1596. list-view? (= display-type :logseq.property.view/type.list)
  1597. group-by-property-ident (or (:db/ident group-by-property)
  1598. (when (and list-view? (nil? group-by-property))
  1599. :block/page)
  1600. (when (and (not db-based?) (contains? #{:linked-references :unlinked-references} view-feature-type))
  1601. :block/page))]
  1602. (run-effects! option table-map *scroller-ref gallery?)
  1603. [:div.flex.flex-col.gap-2.grid
  1604. {:ref *view-ref}
  1605. (ui/foldable
  1606. (view-head view-parent view-entity table columns input sorting set-input! add-new-object! option)
  1607. [:div.ls-view-body.flex.flex-col.gap-2.grid.mt-1
  1608. (filters-row view-entity table option)
  1609. (let [view-opts {:*scroller-ref *scroller-ref
  1610. :display-type display-type
  1611. :row-selection row-selection
  1612. :add-new-object! add-new-object!}]
  1613. (if (and group-by-property-ident (not (number? (first (:rows table)))))
  1614. (when (seq (:rows table))
  1615. [:div.flex.flex-col.border-t.pt-2.gap-2
  1616. (map-indexed
  1617. (fn [idx [value group]]
  1618. (let [add-new-object! (when (fn? add-new-object!)
  1619. (fn [_]
  1620. (add-new-object! view-entity table
  1621. {:properties {(:db/ident group-by-property) (or (and (map? value) (:db/id value)) value)}})))
  1622. table' (shui/table-option (-> table-map
  1623. (assoc-in [:data-fns :add-new-object!] add-new-object!)
  1624. (assoc :data group ; data for this group
  1625. )))
  1626. readable-property-value #(if (and (map? %) (or (:block/title %) (:logseq.property/value %)))
  1627. (db-property/property-value-content %)
  1628. (str %))
  1629. group-by-page? (or (= :block/page group-by-property-ident)
  1630. (and (not db-based?) (contains? #{:linked-references :unlinked-references} display-type)))]
  1631. (rum/with-key
  1632. (ui/foldable
  1633. [:div
  1634. (cond
  1635. group-by-page?
  1636. (if value
  1637. (let [c (state/get-component :block/page-cp)]
  1638. (c {:disable-preview? true} value))
  1639. [:div.text-muted-foreground.text-sm
  1640. "Pages"])
  1641. (some? value)
  1642. (let [icon (pu/get-block-property-value value :logseq.property/icon)]
  1643. [:div.flex.flex-row.gap-1.items-center
  1644. (when icon (icon-component/icon icon {:color? true}))
  1645. (readable-property-value value)])
  1646. :else
  1647. (str "No " (:block/title group-by-property)))]
  1648. (let [render (view-cp view-entity (assoc table' :rows group) option view-opts)]
  1649. (if list-view? [:div.-ml-2 render] render))
  1650. {:title-trigger? false})
  1651. (str (:db/id view-entity) "-group-idx-" idx))))
  1652. (:rows table))])
  1653. (view-cp view-entity table option view-opts)))]
  1654. (merge {:title-trigger? false} foldable-options))]))
  1655. (rum/defcs view-container
  1656. "Provides a view for data like query results and tagged objects, multiple
  1657. layouts such as table and list are supported. Args:
  1658. * view-entity: a db Entity
  1659. * option:
  1660. * title-key: dict key defaults to `:views.table/default-title`
  1661. * data: a collections of entities
  1662. * set-data!: `fn` to update `data`
  1663. * columns: view columns including properties and db attributes, which could be built by `build-columns`
  1664. * add-new-object!: `fn` to create a new object (or row)
  1665. * show-add-property?: whether to show `Add property`
  1666. * add-property!: `fn` to add a new property (or column)"
  1667. < (rum/local nil ::scroller-ref)
  1668. [state view-entity option]
  1669. (rum/with-key (view-inner view-entity
  1670. (cond-> option
  1671. (or config/publishing? (:logseq.property.view/group-by-property view-entity))
  1672. (dissoc :add-new-object!))
  1673. (::scroller-ref state))
  1674. (str "view-" (:db/id view-entity))))
  1675. (defn <load-view-data
  1676. [view opts]
  1677. (state/<invoke-db-worker :thread-api/get-view-data (state/get-current-repo) (:db/id view) opts))
  1678. (rum/defc view-aux
  1679. [view-entity {:keys [view-parent view-feature-type data query-entity-ids set-view-entity!] :as option}]
  1680. (let [[input set-input!] (hooks/use-state "")
  1681. sorting* (:logseq.property.table/sorting view-entity)
  1682. sorting (if (or (= sorting* :logseq.property/empty-placeholder) (empty? sorting*))
  1683. [{:id :block/updated-at, :asc? false}]
  1684. sorting*)
  1685. [sorting set-sorting!] (rum/use-state sorting)
  1686. view-filters (:logseq.property.table/filters view-entity)
  1687. [filters set-filters!] (rum/use-state (or view-filters {}))
  1688. query? (= view-feature-type :query-result)
  1689. [loading? set-loading!] (hooks/use-state (not query?))
  1690. [data set-data!] (hooks/use-state data)
  1691. [ref-pages-count set-ref-pages-count!] (hooks/use-state nil)
  1692. load-view-data (fn load-view-data []
  1693. (c.m/run-task*
  1694. (m/sp
  1695. (let [need-query? (and query? (seq query-entity-ids) (or sorting filters (not (string/blank? input))))]
  1696. (cond
  1697. (and query? (empty? query-entity-ids))
  1698. (set-data! nil)
  1699. (and query? (not (or sorting filters)) (string/blank? input))
  1700. (set-data! query-entity-ids)
  1701. :else
  1702. (when (or (not query?) need-query?)
  1703. (try
  1704. (let [{:keys [data ref-pages-count]}
  1705. (c.m/<?
  1706. (<load-view-data view-entity
  1707. (cond->
  1708. {:view-for-id (or (:db/id (:logseq.property/view-for view-entity))
  1709. (:db/id view-parent))
  1710. :view-feature-type view-feature-type
  1711. :input input
  1712. :filters filters
  1713. :sorting sorting}
  1714. query?
  1715. (assoc :query-entity-ids query-entity-ids))))]
  1716. (set-data! data)
  1717. (when ref-pages-count
  1718. (set-ref-pages-count! ref-pages-count)))
  1719. (finally
  1720. (set-loading! false)))))))))]
  1721. (let [sorting-filters {:sorting sorting
  1722. :filters filters}]
  1723. (hooks/use-effect!
  1724. load-view-data
  1725. [(:db/id view-entity)
  1726. (hooks/use-debounced-value input 300)
  1727. sorting-filters
  1728. (:db/id (:logseq.property.view/group-by-property view-entity))
  1729. (:db/id (:logseq.property.view/type view-entity))
  1730. ;; page filters
  1731. (:logseq.property.linked-references/includes view-parent)
  1732. (:logseq.property.linked-references/excludes view-parent)
  1733. (:filters view-parent)
  1734. query-entity-ids]))
  1735. (if loading?
  1736. [:div.flex.flex-col.space-2.gap-2.my-2
  1737. (repeat 3 (shui/skeleton {:class "h-6 w-full"}))]
  1738. [:div.flex.flex-col.gap-2
  1739. (view-container view-entity (assoc option
  1740. :data data
  1741. :full-data data
  1742. :filters filters
  1743. :sorting sorting
  1744. :set-filters! set-filters!
  1745. :set-sorting! set-sorting!
  1746. :set-data! set-data!
  1747. :set-input! set-input!
  1748. :input input
  1749. :items-count (if (every? number? data)
  1750. (count data)
  1751. ;; grouped
  1752. (reduce (fn [total [_ col]]
  1753. (+ total (count col))) 0 data))
  1754. :ref-pages-count ref-pages-count
  1755. :load-view-data load-view-data
  1756. :set-view-entity! set-view-entity!))])))
  1757. (rum/defc sub-view < rum/reactive db-mixins/query
  1758. [view-entity option]
  1759. (let [view (or (some-> (:db/id view-entity) db/sub-block) view-entity)]
  1760. (view-aux view option)))
  1761. (rum/defc view < rum/static
  1762. [{:keys [view-parent view-feature-type view-entity] :as option}]
  1763. (let [[views set-views!] (hooks/use-state nil)
  1764. [view-entity set-view-entity!] (hooks/use-state view-entity)
  1765. query? (= view-feature-type :query-result)
  1766. db-based? (config/db-based-graph?)]
  1767. (hooks/use-effect!
  1768. #(c.m/run-task*
  1769. (m/sp
  1770. (when-not query? ; TODO: move query logic to worker
  1771. (let [repo (state/get-current-repo)]
  1772. (when (and db-based? (not view-entity))
  1773. (c.m/<? (db-async/<get-views repo (:db/id view-parent) view-feature-type))
  1774. (let [views (get-views view-parent view-feature-type)]
  1775. (if-let [v (first views)]
  1776. (do
  1777. (set-views! views)
  1778. (when-not view-entity (set-view-entity! v)))
  1779. (when (and view-parent view-feature-type (not view-entity))
  1780. (let [new-view (c.m/<? (create-view! view-parent view-feature-type))]
  1781. (set-views! (concat views [new-view]))
  1782. (set-view-entity! new-view))))))))))
  1783. [])
  1784. (when (if db-based? view-entity (or view-entity view-parent
  1785. (= view-feature-type :all-pages)))
  1786. (let [option' (assoc option
  1787. :views views
  1788. :set-views! set-views!
  1789. :set-view-entity! set-view-entity!)]
  1790. (rum/with-key
  1791. (sub-view view-entity option')
  1792. (str "view-" (:db/id view-entity)))))))