views.cljs 105 KB

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