views.cljs 98 KB

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