views.cljs 103 KB

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