diff --git a/deps/shui/src/logseq/shui/table/core.cljc b/deps/shui/src/logseq/shui/table/core.cljc index 4b7ab7315..31f498af2 100644 --- a/deps/shui/src/logseq/shui/table/core.cljc +++ b/deps/shui/src/logseq/shui/table/core.cljc @@ -106,11 +106,13 @@ prop) children])) +;; FIXME: sticky header (rum/defc table-header < rum/static [& prop-and-children] (let [[prop children] (get-prop-and-children prop-and-children)] [: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) children])) diff --git a/deps/shui/src/logseq/shui/ui.cljs b/deps/shui/src/logseq/shui/ui.cljs index f66dca54e..e9b40a3dc 100644 --- a/deps/shui/src/logseq/shui/ui.cljs +++ b/deps/shui/src/logseq/shui/ui.cljs @@ -131,7 +131,6 @@ (def table-option table-core/table-option) (def table table-core/table) (def table-header table-core/table-header) -(def table-head table-core/table-head) (def table-row table-core/table-row) (def table-cell table-core/table-cell) (def table-get-selection-rows table-core/get-selection-rows) diff --git a/src/main/frontend/components/all_pages.cljs b/src/main/frontend/components/all_pages.cljs index ca8e451ff..f3a4c0fc1 100644 --- a/src/main/frontend/components/all_pages.cljs +++ b/src/main/frontend/components/all_pages.cljs @@ -1,230 +1,126 @@ (ns frontend.components.all-pages "All pages" - (:require [logseq.shui.ui :as shui] - [rum.core :as rum] - [frontend.util :as util] - [frontend.ui :as ui] - [clojure.string :as string] + (:require [clojure.string :as string] [frontend.components.block :as component-block] - [frontend.components.page :as component-page] + [frontend.components.views :as views] [frontend.handler.page :as page-handler] [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] - [frontend.search.fuzzy :as fuzzy-search])) - -;; 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)) + [promesa.core :as p] + [rum.core :as rum])) (def columns - [{:id :select - :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 + [{:id :block/original-name :name "Page name" - :header header-cp :cell (fn [_table row _column] - (component-block/page-cp {} row))} + (component-block/page-cp {} row)) + :type :string} {:id :block/type :name "Type" - :header header-cp :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 - :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))))} + :name "Tags"} {:id :block.temp/refs-count :name "Backlinks" - :header header-cp - :cell (fn [_table row _column] (:block.temp/refs-count row))} - {: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}]) + :cell (fn [_table row _column] (:block.temp/refs-count row)) + :type :number}]) + (defn- get-all-pages [] (->> (page-handler/get-all-pages (state/get-current-repo)) (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 [] - (let [[input set-input!] (rum/use-state "") - [sorting set-sorting!] (rum/use-state [{:id :block/updated-at, :asc? false}]) - [row-filter set-row-filter!] (rum/use-state nil) - [visible-columns set-visible-columns!] (rum/use-state {:block/type false}) - [row-selection set-row-selection!] (rum/use-state {}) - [data set-data!] (rum/use-state (get-all-pages)) - _ (rum/use-effect! - (fn [] - (when-let [^js worker @state/*db-worker] - (p/let [result-str (.get-page-refs-count worker (state/get-current-repo)) - result (ldb/read-transit-str result-str) - data (map (fn [row] (assoc row :block.temp/refs-count (get result (:db/id row) 0))) data)] - (set-data! data)))) - []) - table (shui/table-option {:data data - :columns columns - :state {:sorting sorting - :row-filter row-filter - :row-selection row-selection - :visible-columns visible-columns} - :data-fns {:set-sorting! set-sorting! - :set-visible-columns! set-visible-columns! - :set-row-selection! set-row-selection!}}) - selected-rows (shui/table-get-selection-rows row-selection (:rows table)) - 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 [] + (let [[data set-data!] (rum/use-state (get-all-pages)) + columns (views/build-columns {} columns + {:with-object-name? false})] + (rum/use-effect! + (fn [] + (when-let [^js worker @state/*db-worker] + (p/let [result-str (.get-page-refs-count worker (state/get-current-repo)) + result (ldb/read-transit-str result-str) + data (map (fn [row] (assoc row :block.temp/refs-count (get result (:db/id row) 0))) data)] + (set-data! data)))) + []) + [:div.ls-all-pages.max-w-fit.m-auto + (views/view nil {:data data + :set-data! set-data! + :columns columns})])) + +(comment + (rum/defc all-pages < rum/static + [] + (let [[input set-input!] (rum/use-state "") + [sorting set-sorting!] (rum/use-state [{:id :block/updated-at, :asc? false}]) + [row-filter set-row-filter!] (rum/use-state nil) + [visible-columns set-visible-columns!] (rum/use-state {:block/type false}) + [row-selection set-row-selection!] (rum/use-state {}) + [data set-data!] (rum/use-state (get-all-pages)) + _ (rum/use-effect! + (fn [] + (when-let [^js worker @state/*db-worker] + (p/let [result-str (.get-page-refs-count worker (state/get-current-repo)) + result (ldb/read-transit-str result-str) + data (map (fn [row] (assoc row :block.temp/refs-count (get result (:db/id row) 0))) data)] + (set-data! data)))) + []) + table (shui/table-option {:data data + :columns columns + :state {:sorting sorting + :row-filter row-filter + :row-selection row-selection + :visible-columns visible-columns} + :data-fns {:set-sorting! set-sorting! + :set-visible-columns! set-visible-columns! + :set-row-selection! set-row-selection!}}) + selected-rows (shui/table-get-selection-rows row-selection (:rows table)) + 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. ;; https://stackoverflow.com/questions/55621212/is-it-possible-to-react-usestate-in-react - (fn [row] - (if (string/blank? value) - true - (when row - (pos? (fuzzy-search/score (string/lower-case value) (:block/name row)))))))))) - :class "max-w-sm !h-7 !py-0"}) - (columns-select columns table)]] - (let [columns' (:columns table) - rows (:rows table)] - [:div.rounded-md.border - (ui/virtualized-table - {:custom-scroll-parent (gdom/getElement "main-content-container") - :total-count (count rows) - :fixedHeaderContent (fn [] (table-header table columns')) - :components {:Table (fn [props] - (shui/table {} - (.-children props))) - :TableRow (fn [props] (table-row table rows columns' props))}})]) + (fn [row] + (if (string/blank? value) + true + (when row + (pos? (fuzzy-search/score (string/lower-case value) (:block/name row)))))))))) + :class "max-w-sm !h-7 !py-0"}) + (columns-select columns table)]] + (let [columns' (:columns table) + rows (:rows table)] + [:div.rounded-md.border + (ui/virtualized-table + {:custom-scroll-parent (gdom/getElement "main-content-container") + :total-count (count rows) + :fixedHeaderContent (fn [] (table-header table columns')) + :components {:Table (fn [props] + (shui/table {} + (.-children props))) + :TableRow (fn [props] (table-row table rows columns' props))}})]) - (let [rows-count (count (:rows table))] - [:div.flex.items-center.justify-end.space-x-2.py-4 - [:div.flex-1.text-sm.text-muted-foreground - (if (pos? selected-rows-count) - (str selected-rows-count " of " rows-count " row(s) selected.") - (str "Total: " rows-count))]])])) + (let [rows-count (count (:rows table))] + [:div.flex.items-center.justify-end.space-x-2.py-4 + [:div.flex-1.text-sm.text-muted-foreground + (if (pos? selected-rows-count) + (str selected-rows-count " of " rows-count " row(s) selected.") + (str "Total: " rows-count))]])]))) diff --git a/src/main/frontend/components/views.cljs b/src/main/frontend/components/views.cljs index dd95bdb98..955e10ca9 100644 --- a/src/main/frontend/components/views.cljs +++ b/src/main/frontend/components/views.cljs @@ -75,10 +75,10 @@ (cond (uuid? entity) (db-property/property-value-content (db/entity [:block/uuid entity])) - (map? entity) + (de/entity? entity) (db-property/property-value-content entity) :else - (str entity)))) + entity))) (defn- get-property-value-for-search [block property] @@ -95,42 +95,56 @@ (string/join ", " col)))) (defn build-columns - [config properties] + [config properties & {:keys [with-object-name?] + :or {with-object-name? true}}] (let [container-id (state/get-next-container-id)] - (concat - [{:id :select - :name "Select" - :header (fn [table _column] (header-checkbox table)) - :cell (fn [table row column] - (row-checkbox table row column)) - :column-list? false} - {:id :object/name - :name "Name" - :type :string - :header header-cp - :cell (fn [_table row _column] - (component-block/block-container (assoc config :table? true) row)) - :disable-hide? true}] - (map - (fn [property] - {:id (:db/ident property) - :name (:block/original-name property) - :header header-cp - :cell (fn [_table row _column] - (pv/property-value row property (get row (:db/ident property)) {:container-id container-id})) - :get-value (fn [row] (get-property-value-for-search row property))}) - properties) + (->> (concat + [{:id :select + :name "Select" + :header (fn [table _column] (header-checkbox table)) + :cell (fn [table row column] + (row-checkbox table row column)) + :column-list? false} + (when with-object-name? + {:id :object/name + :name "Name" + :type :string + :header header-cp + :cell (fn [_table row _column] + (component-block/block-container (assoc config :table? true) row)) + :disable-hide? true})] + (map + (fn [property] + (let [ident (or (:id property) (:db/ident property)) + property (if (de/entity? property) + property + (or (db/entity ident) property))] + {:id ident + :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 - :name "Created At" - :type :date-time - :header header-cp - :cell timestamp-cell-cp} - {:id :block/updated-at - :name "Updated At" - :type :date-time - :header header-cp - :cell timestamp-cell-cp}]))) + [{:id :block/created-at + :name "Created At" + :type :date-time + :header header-cp + :cell timestamp-cell-cp} + {:id :block/updated-at + :name "Updated At" + :type :date-time + :header header-cp + :cell timestamp-cell-cp}]) + (remove nil?)))) (defn- sort-columns [columns ordered-column-ids] @@ -177,7 +191,7 @@ [column] (case (:id column) :select 32 - :object/name 360 + (:object/name :block/original-name :block/name :block/content) 360 (:block/created-at :block/updated-at) 160 180)) @@ -204,8 +218,9 @@ [{:keys [row-selected?] :as table} rows columns props] (let [idx (gobj/get props "data-index") row (nth rows idx) - row (db/sub-block (:id row)) - row (assoc row :id (:db/id row))] + row' (db/sub-block (:id row)) + ;; merge entity temporal attributes + row (reduce (fn [e [k v]] (assoc e k v)) row' (.-kv ^js row))] (shui/table-row (merge (bean/->clj props) @@ -312,6 +327,7 @@ (rum/defc filter-property < rum/static [columns {:keys [data-fns] :as table}] (let [[property set-property!] (rum/use-state nil) + schema (:schema (db/get-db)) timestamp? (timestamp-property? (:db/ident property)) set-filters! (:set-filters! data-fns) filters (get-in table [:state :filters]) @@ -328,8 +344,11 @@ (let [id (:id column) property (db/entity id) internal-property {:db/ident (:id column) - :block/original-name (:name column)}] - (if (or property (timestamp-property? id)) + :block/original-name (:name column) + :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)) (do (shui/popup-hide!) @@ -364,8 +383,11 @@ :input-default-placeholder (if property (:block/original-name property) "Select") :multiple-choices? true :on-chosen (fn [_value _selected? selected] - (let [filters' (if (seq selected) - (conj filters [(:db/ident property) :is selected]) + (let [selected-value (if (de/entity? (first selected)) + (set (map :block/uuid selected)) + selected) + filters' (if (seq selected) + (conj filters [(:db/ident property) :is selected-value]) filters)] (set-filters! filters')))}))) :else @@ -598,7 +620,7 @@ (filter-value-select table property value operator idx)))) (rum/defc filters-row < rum/static - [{:keys [data-fns] :as table}] + [{:keys [data-fns columns] :as table}] (let [filters (get-in table [:state :filters]) {:keys [set-filters!]} data-fns] (when (seq filters) @@ -609,7 +631,10 @@ property (if (= property-ident :object/name) {:db/ident property-ident :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 (shui/button {:class "!px-2 rounded-none border-r" @@ -650,26 +675,30 @@ (set? value) value (nil? value) #{} :else #{value}) + entity? (de/entity? (first value')) result (case operator :is (if (boolean? match) (= (boolean (get-property-value-content (get row property-ident))) match) - (if (and (empty? match) (empty? value)) + (if (and (empty? match) (empty? value')) true - (when (coll? value) - (boolean (seq (set/intersection (set (map :block/uuid value')) match)))))) + (if entity? + (boolean (seq (set/intersection (set (map :block/uuid value')) match))) + (boolean (seq (set/intersection (set value') match)))))) :is-not (if (boolean? match) (not= (boolean (get-property-value-content (get row property-ident))) match) (cond - (and (empty? match) (seq value)) + (and (empty? match) (seq value')) true - (and (seq match) (empty? value)) + (and (seq match) (empty? value')) true - (coll? value) - (boolean (empty? (set/intersection (set (map :block/uuid value')) match))))) + :else + (if entity? + (boolean (empty? (set/intersection (set (map :block/uuid value')) match))) + (boolean (empty? (set/intersection (set value') match)))))) :text-contains (some #(fuzzy-matched? match (get-property-value-content %)) value') @@ -751,29 +780,31 @@ filters)) (defn- db-set-table-state! - [entity {:keys [set-sorting! set-filters! set-visible-columns! set-ordered-columns!]}] - (let [repo (state/get-current-repo)] - {:set-sorting! - (fn [sorting] - (set-sorting! sorting) - (property-handler/set-block-property! repo (:db/id entity) :logseq.property/table-sorting sorting)) - :set-filters! - (fn [filters] - (let [filters (table-filters->persist-state filters)] - (set-filters! filters) - (property-handler/set-block-property! repo (:db/id entity) :logseq.property/table-filters filters))) - :set-visible-columns! - (fn [columns] - (let [hidden-columns (vec (keep (fn [[column visible?]] - (when (false? visible?) - column)) columns))] - (set-visible-columns! columns) - (property-handler/set-block-property! repo (:db/id entity) :logseq.property/table-hidden-columns hidden-columns))) - :set-ordered-columns! - (fn [ordered-columns] - (let [ids (vec (remove #{:select} ordered-columns))] - (set-ordered-columns! ordered-columns) - (property-handler/set-block-property! repo (:db/id entity) :logseq.property/table-ordered-columns ids)))})) + [entity {:keys [set-sorting! set-filters! set-visible-columns! set-ordered-columns!] :as option}] + (if entity + (let [repo (state/get-current-repo)] + {:set-sorting! + (fn [sorting] + (set-sorting! sorting) + (property-handler/set-block-property! repo (:db/id entity) :logseq.property/table-sorting sorting)) + :set-filters! + (fn [filters] + (let [filters (table-filters->persist-state filters)] + (set-filters! filters) + (property-handler/set-block-property! repo (:db/id entity) :logseq.property/table-filters filters))) + :set-visible-columns! + (fn [columns] + (let [hidden-columns (vec (keep (fn [[column visible?]] + (when (false? visible?) + column)) columns))] + (set-visible-columns! columns) + (property-handler/set-block-property! repo (:db/id entity) :logseq.property/table-hidden-columns hidden-columns))) + :set-ordered-columns! + (fn [ordered-columns] + (let [ids (vec (remove #{:select} ordered-columns))] + (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 [view-entity {:keys [data set-data! columns add-new-object!]}] @@ -841,13 +872,14 @@ (let [columns' (:columns table) rows (:rows table)] [: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 - {:custom-scroll-parent (gdom/getElement "main-content-container") - :total-count (count rows) - :components {:Table (fn [props] - (shui/table {} - (.-children props))) - :TableRow (fn [props] (table-row table rows columns' props))}}) - (when add-new-object! (add-new-row table))])])) + (ui/virtualized-table + {:custom-scroll-parent (gdom/getElement "main-content-container") + :total-count (count rows) + :components {:Table (fn [props] + (shui/table {} + (.-children props))) + :TableRow (fn [props] (table-row table rows columns' props))}}) + (when add-new-object! (add-new-row table))]])]))