فهرست منبع

feat: table columns pinning (#11693)

* Add property :logseq.property.table/pinned-columns

* feat: table column pinning

* enhance: hide :id column by default

---------

Co-authored-by: charlie <[email protected]>
Tienson Qin 9 ماه پیش
والد
کامیت
926d05c185

+ 2 - 2
deps/db/src/logseq/db/frontend/db_ident.cljc

@@ -1,7 +1,7 @@
 (ns logseq.db.frontend.db-ident
   "Helper fns for class and property :db/ident"
-  (:require [datascript.core :as d]
-            [clojure.string :as string]))
+  (:require [clojure.string :as string]
+            [datascript.core :as d]))
 
 (defn ensure-unique-db-ident
   "Ensures the given db-ident is unique. If a db-ident conflicts, it is made

+ 9 - 1
deps/db/src/logseq/db/frontend/property.cljs

@@ -451,7 +451,15 @@
                                            :rtc {:rtc/ignore-attr-when-init-upload true
                                                  :rtc/ignore-attr-when-init-download true
                                                  :rtc/ignore-attr-when-syncing true}}
-
+     :logseq.property.table/pinned-columns {:title "Table view pinned columns"
+                                            :schema
+                                            {:type :property
+                                             :cardinality :many
+                                             :hide? true
+                                             :public? false}
+                                            :rtc {:rtc/ignore-attr-when-init-upload true
+                                                  :rtc/ignore-attr-when-init-download true
+                                                  :rtc/ignore-attr-when-syncing true}}
      :logseq.property/view-for {:title "This view belongs to"
                                 :schema
                                 {:type :node

+ 0 - 1
deps/db/src/logseq/db/frontend/property/build.cljs

@@ -86,7 +86,6 @@
            {:block/title value}))
         common-util/block-with-timestamps)))
 
-
 (defn build-property-values-tx-m
   "Builds a map of property names to their property value blocks to be
   transacted, given a block and a properties map with raw property values. The

+ 1 - 1
deps/db/src/logseq/db/frontend/schema.cljs

@@ -2,7 +2,7 @@
   "Main datascript schemas for the Logseq app"
   (:require [clojure.set :as set]))
 
-(def version 62)
+(def version 63)
 
 ;; A page is a special block, a page can corresponds to multiple files with the same ":block/name".
 (def ^:large-vars/data-var schema

+ 5 - 5
deps/db/src/logseq/db/sqlite/create_graph.cljs

@@ -147,11 +147,11 @@
        (mark-block-as-built-in
         (sqlite-util/build-new-class
          (let [class-properties (mapv
-                                  (fn [db-ident]
-                                    (let [property (get db-ident->properties db-ident)]
-                                      (assert property (str "Built-in property " db-ident " is not defined yet"))
-                                      db-ident))
-                                  (:properties schema))]
+                                 (fn [db-ident]
+                                   (let [property (get db-ident->properties db-ident)]
+                                     (assert property (str "Built-in property " db-ident " is not defined yet"))
+                                     db-ident))
+                                 (:properties schema))]
            (cond->
             {:block/title title'
              :block/name (common-util/page-name-sanity-lc title')

+ 5 - 5
deps/graph-parser/script/db_import.cljs

@@ -6,6 +6,7 @@
             ["fs/promises" :as fsp]
             ["os" :as os]
             ["path" :as node-path]
+            #_:clj-kondo/ignore
             [babashka.cli :as cli]
             [cljs.pprint :as pprint]
             [clojure.set :as set]
@@ -13,7 +14,6 @@
             [datascript.core :as d]
             [logseq.common.graph :as common-graph]
             [logseq.graph-parser.exporter :as gp-exporter]
-            #_:clj-kondo/ignore
             [logseq.outliner.cli :as outliner-cli]
             [logseq.outliner.pipeline :as outliner-pipeline]
             [nbb.classpath :as cp]
@@ -24,11 +24,11 @@
 (def original-transact! d/transact!)
 (defn dev-transact! [conn tx-data tx-meta]
   (swap! tx-queue (fn [queue]
-                        (let [new-queue (conj queue {:tx-data tx-data :tx-meta tx-meta})]
+                    (let [new-queue (conj queue {:tx-data tx-data :tx-meta tx-meta})]
                           ;; Only care about last few so vary 10 as needed
-                          (if (> (count new-queue) 10)
-                            (pop new-queue)
-                            new-queue))))
+                      (if (> (count new-queue) 10)
+                        (pop new-queue)
+                        new-queue))))
   (original-transact! conn tx-data tx-meta))
 
 (defn- build-graph-files

+ 2 - 2
scripts/src/logseq/tasks/db_graph/create_graph_with_properties.cljs

@@ -2,8 +2,8 @@
   "Script that generates all the permutations of property types and cardinality.
    Also creates a page of queries that exercises most properties
    NOTE: This script is also used in CI to confirm graph creation works"
-  (:require ["fs-extra$default" :as fse]
-            ["fs" :as fs]
+  (:require ["fs" :as fs]
+            ["fs-extra$default" :as fse]
             ["os" :as os]
             ["path" :as node-path]
             [babashka.cli :as cli]

+ 15 - 16
src/main/frontend/components/objects.cljs

@@ -180,22 +180,21 @@
                                     (state/pub-event! [:editor/new-property {:block class
                                                                              :class-schema? true}]))
                    ;; Objects of built-in classes must not be deleted e.g. Tag, Property and Root
-                   :on-delete-rows (when-not (:logseq.property/built-in? class)
-                                     (fn [table selected-rows]
-                                       (let [pages (->> selected-rows (filter ldb/page?) (remove :logseq.property/built-in?))
-                                             blocks (->> selected-rows (remove ldb/page?) (remove :logseq.property/built-in?))]
-                                         (p/do!
-                                          (set-data! (get-class-objects class))
-                                          (when-let [f (get-in table [:data-fns :set-row-selection!])]
-                                            (f {}))
-                                          (ui-outliner-tx/transact!
-                                           {:outliner-op :delete-blocks}
-                                           (when (seq blocks)
-                                             (outliner-op/delete-blocks! blocks nil))
-                                           (let [page-ids (map :db/id pages)
-                                                 tx-data (map (fn [pid] [:db/retract pid :block/tags (:db/id class)]) page-ids)]
-                                             (when (seq tx-data)
-                                               (outliner-op/transact! tx-data {:outliner-op :save-block}))))))))}))))
+                   :on-delete-rows (fn [table selected-rows]
+                                     (let [pages (->> selected-rows (filter ldb/page?) (remove :logseq.property/built-in?))
+                                           blocks (->> selected-rows (remove ldb/page?) (remove :logseq.property/built-in?))]
+                                       (p/do!
+                                        (set-data! (get-class-objects class))
+                                        (when-let [f (get-in table [:data-fns :set-row-selection!])]
+                                          (f {}))
+                                        (ui-outliner-tx/transact!
+                                         {:outliner-op :delete-blocks}
+                                         (when (seq blocks)
+                                           (outliner-op/delete-blocks! blocks nil))
+                                         (let [page-ids (map :db/id pages)
+                                               tx-data (map (fn [pid] [:db/retract pid :block/tags (:db/id class)]) page-ids)]
+                                           (when (seq tx-data)
+                                             (outliner-op/transact! tx-data {:outliner-op :save-block})))))))}))))
 
 (rum/defcs class-objects < rum/reactive db-mixins/query mixins/container-id
   [state class {:keys [current-page? sidebar?]}]

+ 20 - 20
src/main/frontend/components/plugins.cljs

@@ -531,25 +531,25 @@
        [:span.opacity-60 "effect?"]]]
      [:div.flex.justify-end.pt-3
       (shui/button
-        {:on-click (fn []
-                     (if (or (string/blank? (util/trim-safe url))
-                           (not (string/starts-with? url "https://")))
-                       (.focus (rum/deref *input))
-                       (let [url (string/replace-first url "https://github.com/" "")
-                             matched (re-find #"([^\/]+)/([^\/]+)" url)]
-                         (if-let [id (some-> matched (nth 2))]
-                           (do
-                             (set-pending! true)
-                             (-> #js {:id id :repo (first matched)
-                                      :theme (:theme? opts)
-                                      :effect (:effect? opts)}
-                               (js/window.logseq.api.__install_plugin)
-                               (p/then #(shui/dialog-close!))
-                               (p/catch #(notification/show! (str %) :error))
-                               (p/finally #(set-pending! false))))
-                           (notification/show! "Invalid GitHub repo url" :error)))))
-         :disabled pending}
-        (if pending (ui/loading "Installing") "Install"))]]))
+       {:on-click (fn []
+                    (if (or (string/blank? (util/trim-safe url))
+                            (not (string/starts-with? url "https://")))
+                      (.focus (rum/deref *input))
+                      (let [url (string/replace-first url "https://github.com/" "")
+                            matched (re-find #"([^\/]+)/([^\/]+)" url)]
+                        (if-let [id (some-> matched (nth 2))]
+                          (do
+                            (set-pending! true)
+                            (-> #js {:id id :repo (first matched)
+                                     :theme (:theme? opts)
+                                     :effect (:effect? opts)}
+                                (js/window.logseq.api.__install_plugin)
+                                (p/then #(shui/dialog-close!))
+                                (p/catch #(notification/show! (str %) :error))
+                                (p/finally #(set-pending! false))))
+                          (notification/show! "Invalid GitHub repo url" :error)))))
+        :disabled pending}
+       (if pending (ui/loading "Installing") "Install"))]]))
 
 (rum/defc auto-check-for-updates-control
   []
@@ -979,7 +979,7 @@
         (lazy-items-loader load-more-pages!)
         [:div.flex.items-center.justify-center.py-28.flex-col.gap-2.opacity-30
          (shui/tabler-icon "list-search" {:size 40})
-         [:span.text-sm "Nothing Founded."]])]]))
+         [:span.text-sm "Nothing Found."]])]]))
 
 (rum/defcs waiting-coming-updates
   < rum/reactive

+ 1 - 1
src/main/frontend/components/property.css

@@ -356,7 +356,7 @@ a.control-link {
   .inner-wrap {
     @apply flex items-center w-full justify-between gap-1 flex-wrap;
 
-    > strong {
+    > .property-setting-title {
       @apply flex items-center gap-1 font-normal opacity-90;
     }
 

+ 20 - 16
src/main/frontend/components/property/config.cljs

@@ -259,7 +259,7 @@
                       item-props)
         [sub-open? set-sub-open!] (rum/use-state false)
         toggle? (boolean? toggle-checked?)
-        id1 (str (or id icon (random-uuid)))
+        id1 (str (or id (random-uuid)))
         id2 (str "d2-" id1)
         or-close-menu-sub! (fn []
                              (when (and (not (shui-popup/get-popup :ls-icon-picker))
@@ -287,7 +287,7 @@
     (wrap-menuitem
      [:div.inner-wrap.cursor-pointer
       {:class (util/classnames [{:disabled disabled?}])}
-      [:strong
+      [:div.property-setting-title
        (some-> icon (name) (shui/tabler-icon {:size 14
                                               :style {:margin-top "-1"}}))
        [:span title]]
@@ -690,11 +690,14 @@
                                                                                                                            :logseq.property/hide?
                                                                                                                            %)}))
                           (when (not (contains? #{:logseq.property/parent :logseq.property.class/properties} (:db/ident property)))
-                            (dropdown-editor-menuitem {:icon :eye-off :title "Hide empty value" :toggle-checked? (boolean (:logseq.property/hide-empty-value property))
-                                                       :disabled? config/publishing?
-                                                       :on-toggle-checked-change #(db-property-handler/set-block-property! (:db/id property)
-                                                                                                                           :logseq.property/hide-empty-value
-                                                                                                                           (not (:logseq.property/hide-empty-value property)))}))]
+                            (dropdown-editor-menuitem
+                             {:icon :eye-off :title "Hide empty value"
+                              :toggle-checked? (boolean (:logseq.property/hide-empty-value property))
+                              :disabled? config/publishing?
+                              :on-toggle-checked-change (fn []
+                                                          (db-property-handler/set-block-property! (:db/id property)
+                                                                                                   :logseq.property/hide-empty-value
+                                                                                                   (not (:logseq.property/hide-empty-value property))))}))]
                          (remove nil?))]
          (when (> (count group') 0)
            (cons (shui/dropdown-menu-separator) group'))))
@@ -718,15 +721,16 @@
               {:icon :checkbox
                :title (if class-schema? "Show as checkbox on tagged nodes" "Show as checkbox on node")
                :disabled? config/publishing?
-               :desc (shui/switch
-                      {:id "show as checkbox" :size "sm"
-                       :checked checked?
-                       :on-click util/stop-propagation
-                       :on-checked-change
-                       (fn [value]
-                         (if value
-                           (db-property-handler/set-block-property! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))
-                           (db-property-handler/delete-property-value! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))))})})))))
+               :desc (when owner-block
+                       (shui/switch
+                        {:id "show as checkbox" :size "sm"
+                         :checked checked?
+                         :on-click util/stop-propagation
+                         :on-checked-change
+                         (fn [value]
+                           (if value
+                             (db-property-handler/set-block-property! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))
+                             (db-property-handler/delete-property-value! (:db/id owner-block) :logseq.property/checkbox-display-properties (:db/id property))))}))})))))
 
      (when (and owner-block
                 ;; Any property should be removable from Tag Properties

+ 10 - 0
src/main/frontend/components/table.css

@@ -129,3 +129,13 @@ html.is-resizing-buf {
 .markdown-table {
   width: 98%;
 }
+
+.sticky-columns {
+    @apply sticky left-0;
+    z-index: 99;
+    background-color: var(--lx-gray-01, var(--ls-primary-background-color, hsl(var(--background))));
+}
+
+.table-action-bar {
+    z-index: 100;
+}

+ 158 - 72
src/main/frontend/components/views.cljs

@@ -8,6 +8,7 @@
             [datascript.impl.entity :as de]
             [dommy.core :as dom]
             [frontend.components.dnd :as dnd]
+            [frontend.components.property.config :as property-config]
             [frontend.components.property.value :as pv]
             [frontend.components.select :as select]
             [frontend.config :as config]
@@ -15,6 +16,7 @@
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db-mixins :as db-mixins]
+            [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.property :as property-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.hooks :as hooks]
@@ -79,15 +81,65 @@
                    (if (or show? checked?) "opacity-100" "opacity-0"))})]))
 
 (defn header-cp
-  [{:keys [column-toggle-sorting! state]} column]
+  [{:keys [view-entity 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)]
+                                    [asc?]))) sorting)
+        property (db/entity (:id column))
+        pinned? (when property
+                  (contains? (set (map :db/id (:logseq.property.table/pinned-columns view-entity)))
+                             (:db/id property)))
+        sub-content (fn [{:keys [id]}]
+                      [:<>
+                       (when property
+                         (shui/dropdown-menu-sub
+                          (shui/dropdown-menu-sub-trigger
+                           [:div.flex.flex-row.items-center.gap-1
+                            (ui/icon "settings" {:size 15})
+                            [:div "Configure"]])
+                          (shui/dropdown-menu-sub-content
+                           [:div.ls-property-dropdown-editor.-m-1
+                            (property-config/dropdown-editor property nil {})])))
+                       (shui/dropdown-menu-sub
+                        (shui/dropdown-menu-sub-trigger
+                         [:div.flex.flex-row.items-center.gap-1
+                          (ui/icon "arrows-down-up" {:size 15})
+                          [:div.mr-4 "Set order"]
+                          (cond asc? [:span.opacity-50.text-sm "ASC"]
+                                (false? asc?) [:span.opacity-50.text-sm "DESC"]
+                                :else nil)])
+                        (shui/dropdown-menu-sub-content
+                         {:on-click #(shui/popup-hide! id)}
+                         (shui/dropdown-menu-item
+                          {:key "asc"
+                           :on-click #(column-toggle-sorting! column)}
+                          "ASC")
+                         (shui/dropdown-menu-item
+                          {:key "desc"
+                           :on-click #(column-toggle-sorting! column)}
+                          "DESC")))
+                       (when property
+                         (shui/dropdown-menu-item
+                          {:on-click (fn [_e]
+                                       (if pinned?
+                                         (db-property-handler/delete-property-value! (:db/id view-entity)
+                                                                                     :logseq.property.table/pinned-columns
+                                                                                     (:db/id property))
+                                         (property-handler/set-block-property! (state/get-current-repo)
+                                                                               (:db/id view-entity)
+                                                                               :logseq.property.table/pinned-columns
+                                                                               (:db/id property)))
+                                       (shui/popup-hide! id))}
+                          [:div.flex.flex-row.items-center.gap-1
+                           (ui/icon "pin" {:size 15})
+                           [:div (if pinned? "Unpin" "Pin")]]))])]
     (shui/button
      {:variant "text"
       :class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start"
-      :on-click #(column-toggle-sorting! column)}
+      :on-mouse-up (fn [^js e]
+                     (when (string/blank? (some-> (.-target e) (.closest "[aria-roledescription=sortable]") (.-style) (.-transform)))
+                       (shui/popup-show! (.-target e) sub-content {:align "start" :as-dropdown? true})))}
      (let [title (str (:name column))]
        [:span {:title title
                :class "max-w-full overflow-hidden text-ellipsis"}
@@ -323,7 +375,7 @@
    (when (fn? on-delete-rows)
      (shui/button
       {:variant "ghost"
-       :class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start"
+       :class "h-8 !pl-0 !px-2 !py-0 text-muted-foreground hover:text-foreground w-full justify-start"
        :on-click (fn []
                    (on-delete-rows table selected-rows))}
       (ui/icon "trash")))))
@@ -410,47 +462,62 @@
      {:data-no-dnd true
       :ref *el}]))
 
-(defn- table-header
-  [table columns {:keys [show-add-property? add-property!] :as option} selected-rows]
-  (let [set-ordered-columns! (get-in table [:data-fns :set-ordered-columns!])
-        set-sized-columns! (get-in table [:data-fns :set-sized-columns!])
+(defn- table-header-cell
+  [table column]
+  (let [header-fn (:header column)
         sized-columns (get-in table [:state :sized-columns])
-        items (mapv (fn [column]
-                      {:id (:name column)
-                       :value (:id column)
-                       :content (let [header-fn (:header column)
-                                      width (get-column-size column sized-columns)
-                                      select? (= :select (:id column))]
-                                  [:div.ls-table-header-cell
-                                   {:style {:width width
-                                            :min-width width}
-                                    :class (when select? "!border-0")}
-                                   (if (fn? header-fn)
-                                     (header-fn table column)
-                                     header-fn)
+        set-sized-columns! (get-in table [:data-fns :set-sized-columns!])
+        width (get-column-size column sized-columns)
+        select? (= :select (:id column))]
+    [:div.ls-table-header-cell
+     {:style {:width width
+              :min-width width}
+      :class (when select? "!border-0")}
+     (if (fn? header-fn)
+       (header-fn table column)
+       header-fn)
                                    ;; resize handle
-                                   (when-not (false? (:resizable? column))
-                                     (column-resizer column
-                                                     (fn [size]
-                                                       (set-sized-columns! (assoc sized-columns (:id column) size)))))])
-                       :disabled? (= (:id column) :select)}) columns)
-        items (if show-add-property?
-                (conj items
-                      {:id "add property"
-                       :prop {:style {:width "-webkit-fill-available"
-                                      :min-width 160}
-                              :on-click (fn [] (when (fn? add-property!) (add-property!)))}
-                       :value :add-new-property
-                       :content (add-property-button)
-                       :disabled? true})
-                items)
+     (when-not (false? (:resizable? column))
+       (column-resizer column
+                       (fn [size]
+                         (set-sized-columns! (assoc sized-columns (:id column) size)))))]))
+
+(defn- table-header
+  [table {:keys [show-add-property? add-property!] :as option} selected-rows]
+  (let [set-ordered-columns! (get-in table [:data-fns :set-ordered-columns!])
+        pinned (get-in table [:state :pinned-columns])
+        unpinned (get-in table [:state :unpinned-columns])
+        build-item (fn [column]
+                     {:id (:name column)
+                      :value (:id column)
+                      :content (table-header-cell table column)
+                      :disabled? (= (:id column) :select)})
+        pinned-items (mapv build-item pinned)
+        unpinned-items (if show-add-property?
+                         (conj (mapv build-item unpinned)
+                               {:id "add property"
+                                :prop {:style {:width "-webkit-fill-available"
+                                               :min-width 160}
+                                       :on-click (fn [] (when (fn? add-property!) (add-property!)))}
+                                :value :add-new-property
+                                :content (add-property-button)
+                                :disabled? true})
+                         (mapv build-item unpinned))
         selection-rows-count (count selected-rows)]
     (shui/table-header
-     (dnd/items items {:vertical? false
-                       :on-drag-end (fn [ordered-columns _m]
-                                      (set-ordered-columns! ordered-columns))})
+     (when (seq pinned-items)
+       [:div.sticky-columns.flex.flex-row
+        (dnd/items pinned-items {:vertical? false
+                                 :on-drag-end (fn [ordered-columns _m]
+                                                (set-ordered-columns! ordered-columns))})])
+     (when (seq unpinned-items)
+       [:div.flex.flex-row
+        (dnd/items unpinned-items
+                   {:vertical? false
+                    :on-drag-end (fn [ordered-columns _m]
+                                   (set-ordered-columns! ordered-columns))})])
      (when (pos? selection-rows-count)
-       [:div.absolute.top-0.left-8
+       [:div.table-action-bar.absolute.top-0.left-8
         (action-bar table selected-rows option)]))))
 
 (rum/defc row-cell < rum/static
@@ -468,41 +535,46 @@
                        (render table row column)))))
 
 (rum/defc table-row-inner < rum/static
-  [{:keys [row-selected?] :as table} row columns props {:keys [show-add-property?]}]
+  [{:keys [row-selected?] :as table} row props {:keys [show-add-property?]}]
   (let [[first-col-rendered? set-first-col-rendered!] (rum/use-state false)
-        columns (if show-add-property?
-                  (conj (vec columns)
-                        {:id :add-property
-                         :cell (fn [_table _row _column])})
-                  columns)
-        sized-columns (get-in table [:state :sized-columns])]
+        pinned-columns (get-in table [:state :pinned-columns])
+        unpinned (get-in table [:state :unpinned-columns])
+        unpinned-columns (if show-add-property?
+                           (conj (vec unpinned)
+                                 {:id :add-property
+                                  :cell (fn [_table _row _column])})
+                           unpinned)
+        sized-columns (get-in table [:state :sized-columns])
+        row-cell-f (fn [idx column]
+                     (let [idx (inc idx)
+                           id (str (:id row) "-" (:id column))
+                           render (get column :cell)
+                           width (get-column-size column sized-columns)
+                           select? (= (:id column) :select)
+                           add-property? (= (:id column) :add-property)
+                           cell-opts {:key id
+                                      :select? select?
+                                      :add-property? add-property?
+                                      :style {:width width
+                                              :min-width width}}]
+                       (when render
+                         (row-cell table row column render cell-opts idx first-col-rendered? set-first-col-rendered!))))]
     (shui/table-row
      (merge
       props
       {:key (str (:id row))
        :data-state (when (row-selected? row) "selected")})
-     (map-indexed
-      (fn [idx column]
-        (let [id (str (:id row) "-" (:id column))
-              render (get column :cell)
-              width (get-column-size column sized-columns)
-              select? (= (:id column) :select)
-              add-property? (= (:id column) :add-property)
-              cell-opts {:key id
-                         :select? select?
-                         :add-property? add-property?
-                         :style {:width width
-                                 :min-width width}}]
-          (when render
-            (row-cell table row column render cell-opts idx first-col-rendered? set-first-col-rendered!))))
-      columns))))
+     [:div.sticky-columns.flex.flex-row
+      (map-indexed row-cell-f pinned-columns)]
+     [:div.flex.flex-row
+      (map-indexed row-cell-f unpinned-columns)])))
 
 (rum/defc table-row < rum/reactive
-  [table row columns props option]
+  [table row props option]
   (let [row' (db/sub-block (:id row))
         ;; merge entity temporal attributes
         row (reduce (fn [e [k v]] (assoc e k v)) row' (.-kv ^js row))]
-    (table-row-inner table row columns props option)))
+    (table-row-inner table row props option)))
 
 (rum/defc search
   [input {:keys [on-change set-input!]}]
@@ -1091,13 +1163,12 @@
      [])
 
     (shui/table
-     (let [columns' (:columns table)
-           rows (:rows table)]
+     (let [rows (:rows table)]
        [:div.ls-table-rows.content.overflow-x-auto.force-visible-scrollbar
         {:ref *rows-wrap}
         (when ready?
           [:div.relative
-           (table-header table columns' option selected-rows)
+           (table-header table option selected-rows)
 
            (ui/virtualized-list
             {:ref #(reset! *scroller-ref %)
@@ -1110,7 +1181,7 @@
              :total-count (count rows)
              :item-content (fn [idx]
                              (let [row (nth rows idx)]
-                               (table-row table row columns' {} option)))})
+                               (table-row table row {} option)))})
 
            (when add-new-object!
              (shui/table-footer (add-new-row table)))])]))))
@@ -1184,7 +1255,7 @@
            (state/set-state! :editor/virtualized-scroll-fn #(ui-handler/scroll-to-anchor-block @*scroller-ref data' gallery?)))))
      [sorting data])))
 
-(rum/defc view-inner < rum/static
+(rum/defc ^:large-vars/cleanup-todo view-inner < rum/static
   [view-entity {:keys [data set-data! columns add-new-object! views-title title-key render-empty-title?] :as option
                 :or {render-empty-title? false}}
    *scroller-ref]
@@ -1193,7 +1264,7 @@
         [sorting set-sorting!] (rum/use-state (or sorting [{:id :block/updated-at, :asc? false}]))
         filters (:logseq.property.table/filters view-entity)
         [filters set-filters!] (rum/use-state (or filters []))
-        default-visible-columns (if-let [hidden-columns (:logseq.property.table/hidden-columns view-entity)]
+        default-visible-columns (if-let [hidden-columns (conj (:logseq.property.table/hidden-columns view-entity) :id)]
                                   (zipmap hidden-columns (repeat false))
                                   ;; This case can happen for imported tables
                                   (if (seq (:logseq.property.table/ordered-columns view-entity))
@@ -1220,7 +1291,20 @@
         [input-filters set-input-filters!] (rum/use-state [input filters])
         [row-selection set-row-selection!] (rum/use-state {})
         columns (sort-columns columns ordered-columns)
-        table-map {:data data
+        select? (first (filter (fn [item] (= (:id item) :select)) columns))
+        id? (first (filter (fn [item] (= (:id item) :id)) columns))
+        pinned-properties (set (cond->> (map :db/ident (:logseq.property.table/pinned-columns view-entity))
+                                 id?
+                                 (cons :id)
+                                 select?
+                                 (cons :select)))
+        {pinned true unpinned false} (group-by (fn [item]
+                                                 (contains? pinned-properties (:id item)))
+                                               (remove (fn [column]
+                                                         (false? (get visible-columns (:id column))))
+                                                       columns))
+        table-map {:view-entity view-entity
+                   :data data
                    :columns columns
                    :state {:sorting sorting
                            :filters filters
@@ -1228,7 +1312,9 @@
                            :row-selection row-selection
                            :visible-columns visible-columns
                            :sized-columns sized-columns
-                           :ordered-columns ordered-columns}
+                           :ordered-columns ordered-columns
+                           :pinned-columns pinned
+                           :unpinned-columns unpinned}
                    :data-fns {:set-data! set-data!
                               :set-row-filter! set-row-filter!
                               :set-filters! set-filters!

+ 1 - 1
src/main/frontend/handler/common/plugin.cljs

@@ -67,7 +67,7 @@
   "Installs plugin given plugin map with id"
   [{:keys [id] :as manifest}]
   (when-not (and (:plugin/installing @state/state)
-              (installed? id))
+                 (installed? id))
     (state/set-state! :plugin/installing manifest)
     (if (util/electron?)
       (ipc/ipc :installMarketPlugin manifest)

+ 23 - 23
src/main/frontend/handler/db_based/page.cljs

@@ -3,6 +3,7 @@
   (:require [clojure.string :as string]
             [datascript.impl.entity :as de]
             [frontend.db :as db]
+            [frontend.db.async :as db-async]
             [frontend.handler.common.page :as page-common-handler]
             [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.editor :as editor-handler]
@@ -12,11 +13,10 @@
             [logseq.common.util.page-ref :as page-ref]
             [logseq.db]
             [logseq.db.frontend.class :as db-class]
-            [logseq.outliner.validate :as outliner-validate]
-            [promesa.core :as p]
-            [frontend.db.async :as db-async]
             [logseq.db.frontend.content :as db-content]
-            [logseq.shui.ui :as shui]))
+            [logseq.outliner.validate :as outliner-validate]
+            [logseq.shui.ui :as shui]
+            [promesa.core :as p]))
 
 (defn- valid-tag?
   "Returns a boolean indicating whether the new tag passes all valid checks.
@@ -60,25 +60,25 @@
   (if (db/page-exists? (:block/title page-entity) #{:logseq.class/Page})
     (notification/show! (str "A page with the name \"" (:block/title page-entity) "\" already exists.") :warning false)
     (when-not (:logseq.property/built-in? page-entity)
-     (p/let [objects (db-async/<get-tag-objects (state/get-current-repo) (:db/id page-entity))]
-       (let [convert-fn
-             (fn convert-fn []
-               (let [page-txs [[:db/retract (:db/id page-entity) :db/ident]
-                               [:db/retract (:db/id page-entity) :block/tags :logseq.class/Tag]
-                               [:db/add (:db/id page-entity) :block/tags :logseq.class/Page]]
-                     obj-txs (mapcat (fn [obj]
-                                       (let [tags (map #(db/entity (state/get-current-repo) (:db/id %)) (:block/tags obj))]
-                                         [{:db/id (:db/id obj)
-                                           :block/title (db-content/replace-tag-refs-with-page-refs (:block/title obj) tags)}
-                                          [:db/retract (:db/id obj) :block/tags (:db/id page-entity)]]))
-                                     objects)
-                     txs (concat page-txs obj-txs)]
-                 (db/transact! (state/get-current-repo) txs {:outliner-op :save-block})))]
-         (-> (shui/dialog-confirm!
-              "Converting a tag to page also removes tags from any nodes that have that tag. Are you ok with that?"
-              {:id :convert-tag-to-page
-               :data-reminder :ok})
-             (p/then convert-fn)))))))
+      (p/let [objects (db-async/<get-tag-objects (state/get-current-repo) (:db/id page-entity))]
+        (let [convert-fn
+              (fn convert-fn []
+                (let [page-txs [[:db/retract (:db/id page-entity) :db/ident]
+                                [:db/retract (:db/id page-entity) :block/tags :logseq.class/Tag]
+                                [:db/add (:db/id page-entity) :block/tags :logseq.class/Page]]
+                      obj-txs (mapcat (fn [obj]
+                                        (let [tags (map #(db/entity (state/get-current-repo) (:db/id %)) (:block/tags obj))]
+                                          [{:db/id (:db/id obj)
+                                            :block/title (db-content/replace-tag-refs-with-page-refs (:block/title obj) tags)}
+                                           [:db/retract (:db/id obj) :block/tags (:db/id page-entity)]]))
+                                      objects)
+                      txs (concat page-txs obj-txs)]
+                  (db/transact! (state/get-current-repo) txs {:outliner-op :save-block})))]
+          (-> (shui/dialog-confirm!
+               "Converting a tag to page also removes tags from any nodes that have that tag. Are you ok with that?"
+               {:id :convert-tag-to-page
+                :data-reminder :ok})
+              (p/then convert-fn)))))))
 
 (defn <create-class!
   "Creates a class page and provides class-specific error handling"

+ 2 - 1
src/main/frontend/worker/db/migrate.cljs

@@ -676,7 +676,8 @@
    [61 {:properties [:logseq.property/type :logseq.property/hide? :logseq.property/public? :logseq.property/view-context :logseq.property/ui-position]
         :fix (rename-properties {:property/schema.classes :logseq.property/classes
                                  :property.value/content :logseq.property/value})}]
-   [62 {:fix remove-block-schema}]])
+   [62 {:fix remove-block-schema}]
+   [63 {:properties [:logseq.property.table/pinned-columns]}]])
 
 (let [max-schema-version (apply max (map first schema-version->updates))]
   (assert (<= db-schema/version max-schema-version))

+ 68 - 68
src/main/logseq/api.cljs

@@ -17,8 +17,8 @@
             [frontend.fs :as fs]
             [frontend.handler.code :as code-handler]
             [frontend.handler.command-palette :as palette-handler]
-            [frontend.handler.common.plugin :as plugin-common-handler]
             [frontend.handler.common.page :as page-common-handler]
+            [frontend.handler.common.plugin :as plugin-common-handler]
             [frontend.handler.config :as config-handler]
             [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.dnd :as editor-dnd-handler]
@@ -896,14 +896,14 @@
   (when-let [k' (and (string? k) (some-> k (sanitize-user-property-name) (keyword)))]
     (let [prefix (-resolve-property-prefix-for-db plugin)]
       (p/let [k (if (qualified-keyword? k') k'
-                  (api-block/get-db-ident-for-user-property-name (str prefix k)))
+                    (api-block/get-db-ident-for-user-property-name (str prefix k)))
               p (db-utils/pull k)] p))))
 
 (defn ^:export get_property
   [k]
   (this-as this
-    (p/let [prop (-get-property this k)]
-      (bean/->js (sdk-utils/normalize-keyword-for-json prop)))))
+           (p/let [prop (-get-property this k)]
+             (bean/->js (sdk-utils/normalize-keyword-for-json prop)))))
 
 (defn ^:export upsert_property
   "schema:
@@ -915,87 +915,87 @@
   "
   [k ^js schema ^js opts]
   (this-as this
-    (when-let [k' (and (string? k) (keyword k))]
-      (let [prefix (when (some-> js/window.LSPlugin (.-PluginLocal) (instance? this))
-                     (str (.-id this) "."))]
-        (p/let [opts (or (some-> opts (bean/->clj)) {})
-                name (or (:name opts) (some-> (str k) (string/trim)))
-                k (if (qualified-keyword? k') k'
-                    (api-block/get-db-ident-for-user-property-name (str prefix k)))
-                schema (or (some-> schema (bean/->clj)
-                             (update-keys #(if (contains? #{:hide :public} %)
-                                             (keyword (str (name %) "?")) %))) {})
-                schema (cond-> schema
-                         (string? (:cardinality schema))
-                         (update :cardinality keyword)
-                         (string? (:type schema))
-                         (update :type keyword))
-                p (db-property-handler/upsert-property! k schema
-                    (cond-> opts
-                      name
-                      (assoc :property-name name)))]
-          (bean/->js (sdk-utils/normalize-keyword-for-json p)))))))
+           (when-let [k' (and (string? k) (keyword k))]
+             (let [prefix (when (some-> js/window.LSPlugin (.-PluginLocal) (instance? this))
+                            (str (.-id this) "."))]
+               (p/let [opts (or (some-> opts (bean/->clj)) {})
+                       name (or (:name opts) (some-> (str k) (string/trim)))
+                       k (if (qualified-keyword? k') k'
+                             (api-block/get-db-ident-for-user-property-name (str prefix k)))
+                       schema (or (some-> schema (bean/->clj)
+                                          (update-keys #(if (contains? #{:hide :public} %)
+                                                          (keyword (str (name %) "?")) %))) {})
+                       schema (cond-> schema
+                                (string? (:cardinality schema))
+                                (update :cardinality keyword)
+                                (string? (:type schema))
+                                (update :type keyword))
+                       p (db-property-handler/upsert-property! k schema
+                                                               (cond-> opts
+                                                                 name
+                                                                 (assoc :property-name name)))]
+                 (bean/->js (sdk-utils/normalize-keyword-for-json p)))))))
 
 (defn ^:export remove_property
   [k]
   (this-as this
-    (p/let [prop (-get-property this k)]
-      (when-let [uuid (:block/uuid prop)]
-        (page-common-handler/<delete! uuid nil nil)))))
+           (p/let [prop (-get-property this k)]
+             (when-let [uuid (:block/uuid prop)]
+               (page-common-handler/<delete! uuid nil nil)))))
 
 ;; block properties
 (defn ^:export upsert_block_property
   [block-uuid keyname value]
   (this-as this
-    (p/let [keyname (sanitize-user-property-name keyname)
-            block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
-            repo (state/get-current-repo)
-            _ (db-async/<get-block repo block-uuid :children? false)
-            db? (config/db-based-graph? repo)
-            key (-> (if (keyword? key) (name keyname) keyname) (util/safe-lower-case))
-            key (if db?
-                  (api-block/get-db-ident-for-user-property-name
-                    (str (-resolve-property-prefix-for-db this) key))
-                  key)
-            _ (when (and db? (not (db-utils/entity key)))
-                (db-property-handler/upsert-property! key {} {:property-name keyname}))]
-      (property-handler/set-block-property! repo block-uuid key value))))
+           (p/let [keyname (sanitize-user-property-name keyname)
+                   block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
+                   repo (state/get-current-repo)
+                   _ (db-async/<get-block repo block-uuid :children? false)
+                   db? (config/db-based-graph? repo)
+                   key (-> (if (keyword? key) (name keyname) keyname) (util/safe-lower-case))
+                   key (if db?
+                         (api-block/get-db-ident-for-user-property-name
+                          (str (-resolve-property-prefix-for-db this) key))
+                         key)
+                   _ (when (and db? (not (db-utils/entity key)))
+                       (db-property-handler/upsert-property! key {} {:property-name keyname}))]
+             (property-handler/set-block-property! repo block-uuid key value))))
 
 (defn ^:export remove_block_property
   [block-uuid key]
   (this-as this
-    (p/let [key (sanitize-user-property-name key)
-            block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
-            _ (db-async/<get-block (state/get-current-repo) block-uuid :children? false)
-            db? (config/db-based-graph? (state/get-current-repo))
-            key-ns? (and (keyword? key) (namespace key))
-            key (if key-ns? key (-> (if (keyword? key) (name key) key) (util/safe-lower-case)))
-            key (if (and db? (not key-ns?))
-                  (api-block/get-db-ident-for-user-property-name
-                    (str (-resolve-property-prefix-for-db this) key))
-                  key)]
-      (property-handler/remove-block-property!
-        (state/get-current-repo)
-        block-uuid key))))
+           (p/let [key (sanitize-user-property-name key)
+                   block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
+                   _ (db-async/<get-block (state/get-current-repo) block-uuid :children? false)
+                   db? (config/db-based-graph? (state/get-current-repo))
+                   key-ns? (and (keyword? key) (namespace key))
+                   key (if key-ns? key (-> (if (keyword? key) (name key) key) (util/safe-lower-case)))
+                   key (if (and db? (not key-ns?))
+                         (api-block/get-db-ident-for-user-property-name
+                          (str (-resolve-property-prefix-for-db this) key))
+                         key)]
+             (property-handler/remove-block-property!
+              (state/get-current-repo)
+              block-uuid key))))
 
 (defn ^:export get_block_property
   [block-uuid key]
   (this-as this
-    (p/let [block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
-            _ (db-async/<get-block (state/get-current-repo) block-uuid :children? false)]
-      (when-let [properties (some-> block-uuid (db-model/get-block-by-uuid) (:block/properties))]
-        (when (seq properties)
-          (let [key (sanitize-user-property-name key)
-                property-name (-> (if (keyword? key) (name key) key) (util/safe-lower-case))
-                property-value (or (get properties key)
-                                 (get properties (keyword property-name))
-                                 (get properties
-                                   (api-block/get-db-ident-for-user-property-name
-                                     (str (-resolve-property-prefix-for-db this) property-name))))
-                property-value (if-let [property-id (:db/id property-value)]
-                                 (db/pull property-id) property-value)
-                ret (sdk-utils/normalize-keyword-for-json property-value)]
-            (bean/->js ret)))))))
+           (p/let [block-uuid (sdk-utils/uuid-or-throw-error block-uuid)
+                   _ (db-async/<get-block (state/get-current-repo) block-uuid :children? false)]
+             (when-let [properties (some-> block-uuid (db-model/get-block-by-uuid) (:block/properties))]
+               (when (seq properties)
+                 (let [key (sanitize-user-property-name key)
+                       property-name (-> (if (keyword? key) (name key) key) (util/safe-lower-case))
+                       property-value (or (get properties key)
+                                          (get properties (keyword property-name))
+                                          (get properties
+                                               (api-block/get-db-ident-for-user-property-name
+                                                (str (-resolve-property-prefix-for-db this) property-name))))
+                       property-value (if-let [property-id (:db/id property-value)]
+                                        (db/pull property-id) property-value)
+                       ret (sdk-utils/normalize-keyword-for-json property-value)]
+                   (bean/->js ret)))))))
 
 (def ^:export get_block_properties
   (fn [block-uuid]

+ 5 - 9
src/test/frontend/worker/undo_redo_test.cljs

@@ -1,5 +1,6 @@
 (ns frontend.worker.undo-redo-test
-  (:require [cljs.pprint :as pp]
+  (:require ["fs" :as fs-node]
+            [cljs.pprint :as pp]
             [clojure.test :as t :refer [deftest is testing use-fixtures]]
             [clojure.test.check.generators :as gen]
             [clojure.walk :as walk]
@@ -8,13 +9,12 @@
             [frontend.test.generators :as t.gen]
             [frontend.test.helper :as test-helper]
             [frontend.worker.fixtures :as worker-fixtures]
+            [frontend.worker.state :as worker-state]
             [frontend.worker.undo-redo :as undo-redo]
             [logseq.db :as ldb]
+            [logseq.db.sqlite.util :as sqlite-util]
             [logseq.outliner.op :as outliner-op]
-            [logseq.outliner.tree :as otree]
-            [frontend.worker.state :as worker-state]
-            ["fs" :as fs-node]
-            [logseq.db.sqlite.util :as sqlite-util]))
+            [logseq.outliner.tree :as otree]))
 
 (def ^:private page-uuid (random-uuid))
 (def ^:private init-data (test-helper/initial-test-page-and-blocks {:page-uuid page-uuid}))
@@ -30,7 +30,6 @@
   (worker-fixtures/listen-test-db-fixture [:gen-undo-ops])
   worker-fixtures/listen-test-db-to-write-tx-log-json-file)
 
-
 (def ^:private gen-non-exist-block-uuid gen/uuid)
 
 (defn- gen-block-uuid
@@ -116,7 +115,6 @@
                  [?left :block/uuid ?left-uuid]]
                db))))
 
-
 (defn- check-block-count
   [{:keys [op tx]} current-db]
   (case (first op)
@@ -281,8 +279,6 @@
         (fs-node/writeFileSync "debug.json" (sqlite-util/write-transit-str data))
         (throw (js/Error "check debug.json"))))))
 
-
-
 (comment
   (deftest debug-test
     (let [{:keys [origin-db db illegal-entity other]}