Use new tables for all pages

feat/tables
Tienson Qin 2024-07-05 15:13:34 +08:00
parent 5fa81817de
commit d523948803
4 changed files with 219 additions and 290 deletions

View File

@ -106,11 +106,13 @@
prop) prop)
children])) children]))
;; FIXME: sticky header
(rum/defc table-header < rum/static (rum/defc table-header < rum/static
[& prop-and-children] [& prop-and-children]
(let [[prop children] (get-prop-and-children prop-and-children)] (let [[prop children] (get-prop-and-children prop-and-children)]
[:div.flex.flex-row.items-center.w-fit [:div.flex.flex-row.items-center.w-fit
(merge {:class "border-y transition-colors bg-gray-01"} (merge {:class "border-y transition-colors bg-gray-01"
:style {:z-index 100}}
prop) prop)
children])) children]))

View File

@ -131,7 +131,6 @@
(def table-option table-core/table-option) (def table-option table-core/table-option)
(def table table-core/table) (def table table-core/table)
(def table-header table-core/table-header) (def table-header table-core/table-header)
(def table-head table-core/table-head)
(def table-row table-core/table-row) (def table-row table-core/table-row)
(def table-cell table-core/table-cell) (def table-cell table-core/table-cell)
(def table-get-selection-rows table-core/get-selection-rows) (def table-get-selection-rows table-core/get-selection-rows)

View File

@ -1,230 +1,126 @@
(ns frontend.components.all-pages (ns frontend.components.all-pages
"All pages" "All pages"
(:require [logseq.shui.ui :as shui] (:require [clojure.string :as string]
[rum.core :as rum]
[frontend.util :as util]
[frontend.ui :as ui]
[clojure.string :as string]
[frontend.components.block :as component-block] [frontend.components.block :as component-block]
[frontend.components.page :as component-page] [frontend.components.views :as views]
[frontend.handler.page :as page-handler] [frontend.handler.page :as page-handler]
[frontend.state :as state] [frontend.state :as state]
[frontend.date :as date]
[goog.object :as gobj]
[goog.dom :as gdom]
[cljs-bean.core :as bean]
[promesa.core :as p]
[logseq.db :as ldb] [logseq.db :as ldb]
[frontend.search.fuzzy :as fuzzy-search])) [promesa.core :as p]
[rum.core :as rum]))
;; columns:
;; page name, tags, backlinks, created at updated at
;; default sort: updated at
(defn header-checkbox [{:keys [selected-all? selected-some? toggle-selected-all!]}]
(shui/checkbox
{:checked (or selected-all? (and selected-some? "indeterminate"))
:on-checked-change toggle-selected-all!
:aria-label "Select all"}))
(defn row-checkbox [{:keys [row-selected? row-toggle-selected!]} row _column]
(shui/checkbox
{:checked (row-selected? row)
:on-checked-change (fn [v] (row-toggle-selected! row v))
:aria-label "Select row"}))
(defn- header-cp
[{:keys [column-toggle-sorting! state]} column]
(let [sorting (:sorting state)
[asc?] (some (fn [item] (when (= (:id item) (:id column))
(when-some [asc? (:asc? item)]
[asc?]))) sorting)]
(shui/button
{:variant "text"
:class "!pl-0 hover:text-foreground"
:onClick #(column-toggle-sorting! column)}
(:name column)
(case asc?
true
(ui/icon "arrow-up")
false
(ui/icon "arrow-down")
nil))))
(comment
(defn- default-cell-cp
[_table row column]
(str (get row (:id column)))))
(defn- timestamp-cell-cp
[_table row column]
(some-> (get row (:id column))
date/int->local-time-2))
(def columns (def columns
[{:id :select [{:id :block/original-name
:name "Select"
:header (fn [table _column] (header-checkbox table))
:cell (fn [table row column] (row-checkbox table row column))
:column-list? false}
{:id :block/original-name
:name "Page name" :name "Page name"
:header header-cp
:cell (fn [_table row _column] :cell (fn [_table row _column]
(component-block/page-cp {} row))} (component-block/page-cp {} row))
:type :string}
{:id :block/type {:id :block/type
:name "Type" :name "Type"
:header header-cp
:cell (fn [_table row _column] [:div.capitalize (string/join ", " (get row :block/type))]) :cell (fn [_table row _column] [:div.capitalize (string/join ", " (get row :block/type))])
:get-value (fn [row] (string/join ", " (get row :block/type)))} :get-value (fn [row] (string/join ", " (get row :block/type)))
:type :string}
{:id :block/tags {:id :block/tags
:name "Tags" :name "Tags"}
:header header-cp
:cell (fn [_table row _column]
(component-block/tags {} row))
:get-value (fn [row] (string/join ", " (map :block/original-name (get row :block/tags))))}
{:id :block.temp/refs-count {:id :block.temp/refs-count
:name "Backlinks" :name "Backlinks"
:header header-cp :cell (fn [_table row _column] (:block.temp/refs-count row))
:cell (fn [_table row _column] (:block.temp/refs-count row))} :type :number}])
{:id :block/created-at
:name "Created At"
:header header-cp
:cell timestamp-cell-cp}
{:id :block/updated-at
:name "Updated At"
:header header-cp
:cell timestamp-cell-cp}])
(defn- get-all-pages (defn- get-all-pages
[] []
(->> (page-handler/get-all-pages (state/get-current-repo)) (->> (page-handler/get-all-pages (state/get-current-repo))
(map (fn [p] (assoc p :id (:db/id p)))))) (map (fn [p] (assoc p :id (:db/id p))))))
(rum/defc columns-select
[columns {:keys [column-visible? column-toggle-visibility]}]
(shui/dropdown-menu
(shui/dropdown-menu-trigger
{:asChild true}
(shui/button
{:variant "outline" :size :sm
:class "text-muted-foreground"}
"Columns"
(ui/icon "chevron-down")))
(shui/dropdown-menu-content
{:align "end"}
(for [column (remove #(false? (:column-list? %)) columns)]
(shui/dropdown-menu-checkbox-item
{:key (str (:id column))
:className "capitalize"
:checked (column-visible? column)
:onCheckedChange #(column-toggle-visibility column %)}
(:name column))))))
(defn table-header
[table columns]
(shui/table-row
{:class "bg-gray-01 shadow"}
(for [column columns]
(let [style (case (:id column)
:block/original-name
{}
:select
{:width 32}
{:width 180})]
(shui/table-head
{:key (str (:id column))
:style style}
(let [header-fn (:header column)]
(if (fn? header-fn)
(header-fn table column)
header-fn)))))))
(defn table-row
[{:keys [row-selected?] :as table} rows columns props]
(let [idx (gobj/get props "data-index")
row (nth rows idx)]
(shui/table-row
(merge
(bean/->clj props)
{:key (str (:id row))
:data-state (when (row-selected? row) "selected")})
(for [column columns]
(let [id (str (:id row) "-" (:id column))
render (get column :cell)]
(shui/table-cell
{:key id}
(render table row column)))))))
(rum/defc all-pages < rum/static (rum/defc all-pages < rum/static
[] []
(let [[input set-input!] (rum/use-state "") (let [[data set-data!] (rum/use-state (get-all-pages))
[sorting set-sorting!] (rum/use-state [{:id :block/updated-at, :asc? false}]) columns (views/build-columns {} columns
[row-filter set-row-filter!] (rum/use-state nil) {:with-object-name? false})]
[visible-columns set-visible-columns!] (rum/use-state {:block/type false}) (rum/use-effect!
[row-selection set-row-selection!] (rum/use-state {}) (fn []
[data set-data!] (rum/use-state (get-all-pages)) (when-let [^js worker @state/*db-worker]
_ (rum/use-effect! (p/let [result-str (.get-page-refs-count worker (state/get-current-repo))
(fn [] result (ldb/read-transit-str result-str)
(when-let [^js worker @state/*db-worker] data (map (fn [row] (assoc row :block.temp/refs-count (get result (:db/id row) 0))) data)]
(p/let [result-str (.get-page-refs-count worker (state/get-current-repo)) (set-data! data))))
result (ldb/read-transit-str result-str) [])
data (map (fn [row] (assoc row :block.temp/refs-count (get result (:db/id row) 0))) data)] [:div.ls-all-pages.max-w-fit.m-auto
(set-data! data)))) (views/view nil {:data data
[]) :set-data! set-data!
table (shui/table-option {:data data :columns columns})]))
:columns columns
:state {:sorting sorting (comment
:row-filter row-filter (rum/defc all-pages < rum/static
:row-selection row-selection []
:visible-columns visible-columns} (let [[input set-input!] (rum/use-state "")
:data-fns {:set-sorting! set-sorting! [sorting set-sorting!] (rum/use-state [{:id :block/updated-at, :asc? false}])
:set-visible-columns! set-visible-columns! [row-filter set-row-filter!] (rum/use-state nil)
:set-row-selection! set-row-selection!}}) [visible-columns set-visible-columns!] (rum/use-state {:block/type false})
selected-rows (shui/table-get-selection-rows row-selection (:rows table)) [row-selection set-row-selection!] (rum/use-state {})
selected-rows-count (count selected-rows) [data set-data!] (rum/use-state (get-all-pages))
selected? (pos? selected-rows-count)] _ (rum/use-effect!
[:div.w-full (fn []
[:div.flex.items-center.pb-4.justify-between (when-let [^js worker @state/*db-worker]
[:div.ml-1 (p/let [result-str (.get-page-refs-count worker (state/get-current-repo))
(when selected? result (ldb/read-transit-str result-str)
(shui/button {:variant :destructive data (map (fn [row] (assoc row :block.temp/refs-count (get result (:db/id row) 0))) data)]
:class "text-red-500" (set-data! data))))
:size :sm [])
:on-click #(shui/dialog-open! table (shui/table-option {:data data
(component-page/batch-delete-dialog selected-rows false (fn [] (set-data! (get-all-pages)))))} :columns columns
(ui/icon "trash-x")))] :state {:sorting sorting
[:div.flex.items-center.gap-2 :row-filter row-filter
(shui/input :row-selection row-selection
{:placeholder "Search pages" :visible-columns visible-columns}
:value input :data-fns {:set-sorting! set-sorting!
:onChange (fn [e] :set-visible-columns! set-visible-columns!
(let [value (util/evalue e)] :set-row-selection! set-row-selection!}})
(set-input! value) selected-rows (shui/table-get-selection-rows row-selection (:rows table))
(set-row-filter! (fn [] selected-rows-count (count selected-rows)
selected? (pos? selected-rows-count)]
[:div.w-full
[:div.flex.items-center.pb-4.justify-between
[:div.ml-1
(when selected?
(shui/button {:variant :destructive
:class "text-red-500"
:size :sm
:on-click #(shui/dialog-open!
(component-page/batch-delete-dialog selected-rows false (fn [] (set-data! (get-all-pages)))))}
(ui/icon "trash-x")))]
[:div.flex.items-center.gap-2
(shui/input
{:placeholder "Search pages"
:value input
:onChange (fn [e]
(let [value (util/evalue e)]
(set-input! value)
(set-row-filter! (fn []
;; Returns a fn here. ;; Returns a fn here.
;; https://stackoverflow.com/questions/55621212/is-it-possible-to-react-usestate-in-react ;; https://stackoverflow.com/questions/55621212/is-it-possible-to-react-usestate-in-react
(fn [row] (fn [row]
(if (string/blank? value) (if (string/blank? value)
true true
(when row (when row
(pos? (fuzzy-search/score (string/lower-case value) (:block/name row)))))))))) (pos? (fuzzy-search/score (string/lower-case value) (:block/name row))))))))))
:class "max-w-sm !h-7 !py-0"}) :class "max-w-sm !h-7 !py-0"})
(columns-select columns table)]] (columns-select columns table)]]
(let [columns' (:columns table) (let [columns' (:columns table)
rows (:rows table)] rows (:rows table)]
[:div.rounded-md.border [:div.rounded-md.border
(ui/virtualized-table (ui/virtualized-table
{:custom-scroll-parent (gdom/getElement "main-content-container") {:custom-scroll-parent (gdom/getElement "main-content-container")
:total-count (count rows) :total-count (count rows)
:fixedHeaderContent (fn [] (table-header table columns')) :fixedHeaderContent (fn [] (table-header table columns'))
:components {:Table (fn [props] :components {:Table (fn [props]
(shui/table {} (shui/table {}
(.-children props))) (.-children props)))
:TableRow (fn [props] (table-row table rows columns' props))}})]) :TableRow (fn [props] (table-row table rows columns' props))}})])
(let [rows-count (count (:rows table))] (let [rows-count (count (:rows table))]
[:div.flex.items-center.justify-end.space-x-2.py-4 [:div.flex.items-center.justify-end.space-x-2.py-4
[:div.flex-1.text-sm.text-muted-foreground [:div.flex-1.text-sm.text-muted-foreground
(if (pos? selected-rows-count) (if (pos? selected-rows-count)
(str selected-rows-count " of " rows-count " row(s) selected.") (str selected-rows-count " of " rows-count " row(s) selected.")
(str "Total: " rows-count))]])])) (str "Total: " rows-count))]])])))

View File

@ -75,10 +75,10 @@
(cond (cond
(uuid? entity) (uuid? entity)
(db-property/property-value-content (db/entity [:block/uuid entity])) (db-property/property-value-content (db/entity [:block/uuid entity]))
(map? entity) (de/entity? entity)
(db-property/property-value-content entity) (db-property/property-value-content entity)
:else :else
(str entity)))) entity)))
(defn- get-property-value-for-search (defn- get-property-value-for-search
[block property] [block property]
@ -95,42 +95,56 @@
(string/join ", " col)))) (string/join ", " col))))
(defn build-columns (defn build-columns
[config properties] [config properties & {:keys [with-object-name?]
:or {with-object-name? true}}]
(let [container-id (state/get-next-container-id)] (let [container-id (state/get-next-container-id)]
(concat (->> (concat
[{:id :select [{:id :select
:name "Select" :name "Select"
:header (fn [table _column] (header-checkbox table)) :header (fn [table _column] (header-checkbox table))
:cell (fn [table row column] :cell (fn [table row column]
(row-checkbox table row column)) (row-checkbox table row column))
:column-list? false} :column-list? false}
{:id :object/name (when with-object-name?
:name "Name" {:id :object/name
:type :string :name "Name"
:header header-cp :type :string
:cell (fn [_table row _column] :header header-cp
(component-block/block-container (assoc config :table? true) row)) :cell (fn [_table row _column]
:disable-hide? true}] (component-block/block-container (assoc config :table? true) row))
(map :disable-hide? true})]
(fn [property] (map
{:id (:db/ident property) (fn [property]
:name (:block/original-name property) (let [ident (or (:id property) (:db/ident property))
:header header-cp property (if (de/entity? property)
:cell (fn [_table row _column] property
(pv/property-value row property (get row (:db/ident property)) {:container-id container-id})) (or (db/entity ident) property))]
:get-value (fn [row] (get-property-value-for-search row property))}) {:id ident
properties) :name (or (:name property)
(:block/original-name property))
:header (or (:header property)
header-cp)
:cell (or (:cell property)
(when (de/entity? property)
(fn [_table row _column]
(pv/property-value row property (get row (:db/ident property)) {:container-id container-id}))))
:get-value (or (:get-value property)
(when (de/entity? property)
(fn [row] (get-property-value-for-search row property))))
:type (:type property)}))
properties)
[{:id :block/created-at [{:id :block/created-at
:name "Created At" :name "Created At"
:type :date-time :type :date-time
:header header-cp :header header-cp
:cell timestamp-cell-cp} :cell timestamp-cell-cp}
{:id :block/updated-at {:id :block/updated-at
:name "Updated At" :name "Updated At"
:type :date-time :type :date-time
:header header-cp :header header-cp
:cell timestamp-cell-cp}]))) :cell timestamp-cell-cp}])
(remove nil?))))
(defn- sort-columns (defn- sort-columns
[columns ordered-column-ids] [columns ordered-column-ids]
@ -177,7 +191,7 @@
[column] [column]
(case (:id column) (case (:id column)
:select 32 :select 32
:object/name 360 (:object/name :block/original-name :block/name :block/content) 360
(:block/created-at :block/updated-at) 160 (:block/created-at :block/updated-at) 160
180)) 180))
@ -204,8 +218,9 @@
[{:keys [row-selected?] :as table} rows columns props] [{:keys [row-selected?] :as table} rows columns props]
(let [idx (gobj/get props "data-index") (let [idx (gobj/get props "data-index")
row (nth rows idx) row (nth rows idx)
row (db/sub-block (:id row)) row' (db/sub-block (:id row))
row (assoc row :id (:db/id row))] ;; merge entity temporal attributes
row (reduce (fn [e [k v]] (assoc e k v)) row' (.-kv ^js row))]
(shui/table-row (shui/table-row
(merge (merge
(bean/->clj props) (bean/->clj props)
@ -312,6 +327,7 @@
(rum/defc filter-property < rum/static (rum/defc filter-property < rum/static
[columns {:keys [data-fns] :as table}] [columns {:keys [data-fns] :as table}]
(let [[property set-property!] (rum/use-state nil) (let [[property set-property!] (rum/use-state nil)
schema (:schema (db/get-db))
timestamp? (timestamp-property? (:db/ident property)) timestamp? (timestamp-property? (:db/ident property))
set-filters! (:set-filters! data-fns) set-filters! (:set-filters! data-fns)
filters (get-in table [:state :filters]) filters (get-in table [:state :filters])
@ -328,8 +344,11 @@
(let [id (:id column) (let [id (:id column)
property (db/entity id) property (db/entity id)
internal-property {:db/ident (:id column) internal-property {:db/ident (:id column)
:block/original-name (:name column)}] :block/original-name (:name column)
(if (or property (timestamp-property? id)) :block/schema {:type (:type column)}}]
(if (or property
(= :db.cardinality/many (:db/cardinality (get schema id)))
(not= (:type column) :string))
(set-property! (or property internal-property)) (set-property! (or property internal-property))
(do (do
(shui/popup-hide!) (shui/popup-hide!)
@ -364,8 +383,11 @@
:input-default-placeholder (if property (:block/original-name property) "Select") :input-default-placeholder (if property (:block/original-name property) "Select")
:multiple-choices? true :multiple-choices? true
:on-chosen (fn [_value _selected? selected] :on-chosen (fn [_value _selected? selected]
(let [filters' (if (seq selected) (let [selected-value (if (de/entity? (first selected))
(conj filters [(:db/ident property) :is selected]) (set (map :block/uuid selected))
selected)
filters' (if (seq selected)
(conj filters [(:db/ident property) :is selected-value])
filters)] filters)]
(set-filters! filters')))}))) (set-filters! filters')))})))
:else :else
@ -598,7 +620,7 @@
(filter-value-select table property value operator idx)))) (filter-value-select table property value operator idx))))
(rum/defc filters-row < rum/static (rum/defc filters-row < rum/static
[{:keys [data-fns] :as table}] [{:keys [data-fns columns] :as table}]
(let [filters (get-in table [:state :filters]) (let [filters (get-in table [:state :filters])
{:keys [set-filters!]} data-fns] {:keys [set-filters!]} data-fns]
(when (seq filters) (when (seq filters)
@ -609,7 +631,10 @@
property (if (= property-ident :object/name) property (if (= property-ident :object/name)
{:db/ident property-ident {:db/ident property-ident
:block/original-name "Name"} :block/original-name "Name"}
(db/entity property-ident))] (or (db/entity property-ident)
(some (fn [column] (when (= (:id column) property-ident)
{:db/ident (:id column)
:block/original-name (:name column)})) columns)))]
[:div.flex.flex-row.items-center.border.rounded [:div.flex.flex-row.items-center.border.rounded
(shui/button (shui/button
{:class "!px-2 rounded-none border-r" {:class "!px-2 rounded-none border-r"
@ -650,26 +675,30 @@
(set? value) value (set? value) value
(nil? value) #{} (nil? value) #{}
:else #{value}) :else #{value})
entity? (de/entity? (first value'))
result result
(case operator (case operator
:is :is
(if (boolean? match) (if (boolean? match)
(= (boolean (get-property-value-content (get row property-ident))) match) (= (boolean (get-property-value-content (get row property-ident))) match)
(if (and (empty? match) (empty? value)) (if (and (empty? match) (empty? value'))
true true
(when (coll? value) (if entity?
(boolean (seq (set/intersection (set (map :block/uuid value')) match)))))) (boolean (seq (set/intersection (set (map :block/uuid value')) match)))
(boolean (seq (set/intersection (set value') match))))))
:is-not :is-not
(if (boolean? match) (if (boolean? match)
(not= (boolean (get-property-value-content (get row property-ident))) match) (not= (boolean (get-property-value-content (get row property-ident))) match)
(cond (cond
(and (empty? match) (seq value)) (and (empty? match) (seq value'))
true true
(and (seq match) (empty? value)) (and (seq match) (empty? value'))
true true
(coll? value) :else
(boolean (empty? (set/intersection (set (map :block/uuid value')) match))))) (if entity?
(boolean (empty? (set/intersection (set (map :block/uuid value')) match)))
(boolean (empty? (set/intersection (set value') match))))))
:text-contains :text-contains
(some #(fuzzy-matched? match (get-property-value-content %)) value') (some #(fuzzy-matched? match (get-property-value-content %)) value')
@ -751,29 +780,31 @@
filters)) filters))
(defn- db-set-table-state! (defn- db-set-table-state!
[entity {:keys [set-sorting! set-filters! set-visible-columns! set-ordered-columns!]}] [entity {:keys [set-sorting! set-filters! set-visible-columns! set-ordered-columns!] :as option}]
(let [repo (state/get-current-repo)] (if entity
{:set-sorting! (let [repo (state/get-current-repo)]
(fn [sorting] {:set-sorting!
(set-sorting! sorting) (fn [sorting]
(property-handler/set-block-property! repo (:db/id entity) :logseq.property/table-sorting sorting)) (set-sorting! sorting)
:set-filters! (property-handler/set-block-property! repo (:db/id entity) :logseq.property/table-sorting sorting))
(fn [filters] :set-filters!
(let [filters (table-filters->persist-state filters)] (fn [filters]
(set-filters! filters) (let [filters (table-filters->persist-state filters)]
(property-handler/set-block-property! repo (:db/id entity) :logseq.property/table-filters filters))) (set-filters! filters)
:set-visible-columns! (property-handler/set-block-property! repo (:db/id entity) :logseq.property/table-filters filters)))
(fn [columns] :set-visible-columns!
(let [hidden-columns (vec (keep (fn [[column visible?]] (fn [columns]
(when (false? visible?) (let [hidden-columns (vec (keep (fn [[column visible?]]
column)) columns))] (when (false? visible?)
(set-visible-columns! columns) column)) columns))]
(property-handler/set-block-property! repo (:db/id entity) :logseq.property/table-hidden-columns hidden-columns))) (set-visible-columns! columns)
:set-ordered-columns! (property-handler/set-block-property! repo (:db/id entity) :logseq.property/table-hidden-columns hidden-columns)))
(fn [ordered-columns] :set-ordered-columns!
(let [ids (vec (remove #{:select} ordered-columns))] (fn [ordered-columns]
(set-ordered-columns! ordered-columns) (let [ids (vec (remove #{:select} ordered-columns))]
(property-handler/set-block-property! repo (:db/id entity) :logseq.property/table-ordered-columns ids)))})) (set-ordered-columns! ordered-columns)
(property-handler/set-block-property! repo (:db/id entity) :logseq.property/table-ordered-columns ids)))})
option))
(rum/defc view < rum/static (rum/defc view < rum/static
[view-entity {:keys [data set-data! columns add-new-object!]}] [view-entity {:keys [data set-data! columns add-new-object!]}]
@ -841,13 +872,14 @@
(let [columns' (:columns table) (let [columns' (:columns table)
rows (:rows table)] rows (:rows table)]
[:div.ls-table-rows.rounded-md.content.overflow-x-auto.force-visible-scrollbar [:div.ls-table-rows.rounded-md.content.overflow-x-auto.force-visible-scrollbar
(table-header table columns') [:div.relative
(table-header table columns')
(ui/virtualized-table (ui/virtualized-table
{:custom-scroll-parent (gdom/getElement "main-content-container") {:custom-scroll-parent (gdom/getElement "main-content-container")
:total-count (count rows) :total-count (count rows)
:components {:Table (fn [props] :components {:Table (fn [props]
(shui/table {} (shui/table {}
(.-children props))) (.-children props)))
:TableRow (fn [props] (table-row table rows columns' props))}}) :TableRow (fn [props] (table-row table rows columns' props))}})
(when add-new-object! (add-new-row table))])])) (when add-new-object! (add-new-row table))]])]))