瀏覽代碼

Feat: view group by (#11731)

* wip: view group by

* feat: view group by rendering

* enhance: use current group's value when creating new block

* enhance: add icons for view layouts

* fix: group rows selection

* fix: property type migration from #11695

* refactor: use namespaced keyword for block export

instead of confusing :build/block. Also improved related tests,
simplified import steps, added some missing sqlite.build docs,
and fixed :build/uuid not working for some existing journals.

* fix: remove export+imports invalid

when uuids are kept and when journals are created.
Also removed :logseq.class/Journal from export as its needless noise

* fix(ux): incorrect behavior for the sub menu content within the table header popup

* fix: group rows delete

* fix: lint

* fix: group block delete

* fix: bump version

* fix: group by titles not correct for :checkbox

Also enable group-by for :date as they just work

* fix: disable nonsensical and unreadable groupings for :many properties

* fix: grouping not working for :default property type

and sometimes :number or :url.
Was grouping by entity and not what user reads. If there are
2 :default values with 'text ha', this seemed buggy

* fix: icons not showing for grouping by status

---------

Co-authored-by: Gabriel Horner <[email protected]>
Co-authored-by: charlie <[email protected]>
Tienson Qin 8 月之前
父節點
當前提交
e7e4294088

+ 17 - 5
deps/db/src/logseq/db/frontend/property.cljs

@@ -393,19 +393,31 @@
        :public? false
        :hide? true}
       :closed-values
-      (mapv (fn [[db-ident value]]
+      (mapv (fn [[db-ident value icon]]
               {:db-ident db-ident
                :value value
-               :uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)})
-            [[:logseq.property.view/type.table "Table View"]
-             [:logseq.property.view/type.list "List View"]
-             [:logseq.property.view/type.gallery "Gallery View"]])
+               :uuid (common-uuid/gen-uuid :db-ident-block-uuid db-ident)
+               :icon {:type :tabler-icon :id icon}})
+            [[:logseq.property.view/type.table "Table View" "table"]
+             [:logseq.property.view/type.list "List View" "list"]
+             [:logseq.property.view/type.gallery "Gallery View" "layout-grid"]])
       :properties {:logseq.property/default-value :logseq.property.view/type.table}
       :queryable? true
       :rtc {:rtc/ignore-attr-when-init-upload true
             :rtc/ignore-attr-when-init-download true
             :rtc/ignore-attr-when-syncing true}}
 
+     :logseq.property.view/group-by-property
+     {:title "View group by property"
+      :schema
+      {:type :property
+       :public? false
+       :hide? true}
+      :queryable? true
+      :rtc {:rtc/ignore-attr-when-init-upload true
+            :rtc/ignore-attr-when-init-download true
+            :rtc/ignore-attr-when-syncing true}}
+
      :logseq.property.table/sorting {:title "View sorting"
                                      :schema
                                      {:type :coll

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

@@ -38,7 +38,7 @@
       (throw (js/Error. (str "Cannot compare " x " to " y))))
     (compare-schema-version (parse-schema-version x) y)))
 
-(def version (parse-schema-version "64"))
+(def version (parse-schema-version "64.1"))
 
 (defn major-version
   "Return a number.

+ 1 - 1
deps/db/src/logseq/db/sqlite/export.cljs

@@ -434,4 +434,4 @@
         (js/console.error :property-conflicts @property-conflicts)
         {:error (str "The following imported properties conflict with the current graph: "
                      (pr-str (mapv :property-id @property-conflicts)))})
-      (sqlite-build/build-blocks-tx export-map'))))
+      (sqlite-build/build-blocks-tx export-map'))))

+ 4 - 4
deps/db/test/logseq/db/sqlite/export_test.cljs

@@ -2,11 +2,11 @@
   (:require [cljs.pprint]
             [cljs.test :refer [deftest is testing]]
             [datascript.core :as d]
+            [logseq.common.util.date-time :as date-time-util]
             [logseq.common.util.page-ref :as page-ref]
+            [logseq.db.frontend.validate :as db-validate]
             [logseq.db.sqlite.export :as sqlite-export]
-            [logseq.db.test.helper :as db-test]
-            [logseq.common.util.date-time :as date-time-util]
-            [logseq.db.frontend.validate :as db-validate]))
+            [logseq.db.test.helper :as db-test]))
 
 (defn- export-block-and-import-to-another-block
   "Exports given block from one graph/conn, imports it to a 2nd block and then
@@ -342,4 +342,4 @@
         imported-ontology (sqlite-export/build-export @conn2 {:export-type :graph-ontology})]
 
     (is (= (:properties original-data) (:properties imported-ontology)))
-    (is (= (:classes original-data) (:classes imported-ontology)))))
+    (is (= (:classes original-data) (:classes imported-ontology)))))

+ 34 - 8
deps/shui/src/logseq/shui/table/core.cljc

@@ -1,6 +1,7 @@
 (ns logseq.shui.table.core
   "Table"
-  (:require [dommy.core :refer-macros [sel1]]
+  (:require [clojure.set :as set]
+            [dommy.core :refer-macros [sel1]]
             [logseq.shui.table.impl :as impl]
             [rum.core :as rum]))
 
@@ -27,15 +28,38 @@
   [row-selection rows]
   (boolean
    (or
-    (seq (:selected-ids row-selection))
+    (and (seq (:selected-ids row-selection))
+         (some (:selected-ids row-selection) (map :db/id rows)))
     (and (seq (:exclude-ids row-selection))
          (not= (count rows) (count (:exclude-ids row-selection)))))))
 
+(defn- select-all?
+  [row-selection rows]
+  (set/subset? (set (map :db/id rows))
+               (:selected-ids row-selection)))
+
 (defn- toggle-selected-all!
-  [value set-row-selection!]
-  (if value
-    (set-row-selection! {:selected-all? value})
-    (set-row-selection! {})))
+  [table value set-row-selection!]
+  (let [group-by-property (get-in table [:state :group-by-property])
+        row-selection (get-in table [:state :row-selection])]
+    (cond
+      (and group-by-property value)
+      (let [new-selection (update row-selection :selected-ids
+                                  (fn [ids]
+                                    (set/union (set ids) (set (map :db/id (:rows table))))))]
+        (set-row-selection! new-selection))
+
+      value
+      (set-row-selection! {:selected-all? value})
+
+      group-by-property
+      (let [new-selection (update row-selection :selected-ids
+                                  (fn [ids]
+                                    (set/difference (set ids) (set (map :db/id (:rows table))))))]
+        (set-row-selection! new-selection))
+
+      :else
+      (set-row-selection! {}))))
 
 (defn- set-conj
   [col item]
@@ -96,11 +120,13 @@
            ;; fns
            :column-visible? (fn [column] (impl/column-visible? column visible-columns))
            :column-toggle-visibility (fn [column v] (set-visible-columns! (assoc visible-columns (impl/column-id column) v)))
-           :selected-all? (:selected-all? row-selection)
+           :selected-all? (or (:selected-all? row-selection)
+                              (select-all? row-selection filtered-rows))
            :selected-some? (select-some? row-selection filtered-rows)
            :row-selected? (fn [row] (row-selected? row row-selection))
            :row-toggle-selected! (fn [row value] (row-toggle-selected! row value set-row-selection! row-selection))
-           :toggle-selected-all! (fn [value] (toggle-selected-all! value set-row-selection!))
+           :toggle-selected-all! (fn [table value]
+                                   (toggle-selected-all! table value set-row-selection!))
            :column-set-sorting! (fn [sorting column asc?] (column-set-sorting! column set-sorting! sorting asc?)))))
 
 (defn- get-prop-and-children

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

@@ -1,6 +1,7 @@
 (ns frontend.components.objects
   "Provides table views for class objects and property related objects"
   (:require [frontend.components.filepicker :as filepicker]
+            [frontend.components.icon :as icon-component]
             [frontend.components.views :as views]
             [frontend.db :as db]
             [frontend.db-mixins :as db-mixins]
@@ -17,11 +18,11 @@
             [frontend.util :as util]
             [logseq.common.config :as common-config]
             [logseq.db :as ldb]
+            [logseq.db.frontend.property :as db-property]
             [logseq.outliner.property :as outliner-property]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
-            [rum.core :as rum]
-            [logseq.db.frontend.property :as db-property]))
+            [rum.core :as rum]))
 
 (defn- get-class-objects
   [class]
@@ -29,13 +30,14 @@
        (map (fn [row] (assoc row :id (:db/id row))))))
 
 (defn- add-new-class-object!
-  [class set-data!]
+  [class set-data! properties]
   (p/let [block (editor-handler/api-insert-new-block! ""
                                                       {:page (:block/uuid class)
-                                                       :properties {:block/tags (:db/id class)}
+                                                       :properties (merge properties {:block/tags (:db/id class)})
                                                        :edit-block? false})
           _ (set-data! (get-class-objects class))]
-    (editor-handler/edit-block! (db/entity [:block/uuid (:block/uuid block)]) 0 :unknown-container)))
+    (editor-handler/edit-block! (db/entity [:block/uuid (:block/uuid block)]) 0 {:container-id :unknown-container})
+    block))
 
 (defn- get-views
   [ent]
@@ -88,6 +90,10 @@
                          :align "start"
                          :content-props {:onClick shui/popup-hide!}})
                        (set-view-entity! view)))}
+        (let [display-type (or (:db/ident (get view :logseq.property.view/type))
+                               :logseq.property.view/type.table)]
+          (when-let [icon (:logseq.property/icon (db/entity display-type))]
+            (icon-component/icon icon {:color? true})))
         (if (= (:db/id view) (:db/id class))
           "All"
           (let [title (:block/title view)]
@@ -162,8 +168,8 @@
                    :views-title (class-views class views view-entity {:set-view-entity! set-view-entity!
                                                                       :set-views! set-views!})
                    :columns columns
-                   :add-new-object! (if (= :logseq.class/Asset (:db/ident class))
-                                      (fn [_e]
+                   :add-new-object! (fn [{:keys [properties]}]
+                                      (if (= :logseq.class/Asset (:db/ident class))
                                         (shui/dialog-open!
                                          (fn []
                                            [:div.flex.flex-col.gap-2
@@ -173,8 +179,8 @@
                                                            (p/do!
                                                             (editor-handler/upload-asset! nil files :markdown editor-handler/*asset-uploading? true)
                                                             (set-data! (get-class-objects class))
-                                                            (shui/dialog-close!)))})])))
-                                      #(add-new-class-object! class set-data!))
+                                                            (shui/dialog-close!)))})]))
+                                        (add-new-class-object! class set-data! properties)))
                    :show-add-property? true
                    :add-property! (fn []
                                     (state/pub-event! [:editor/new-property {:block class
@@ -184,7 +190,6 @@
                                      (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!
@@ -194,7 +199,8 @@
                                          (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})))))))}))))
+                                             (outliner-op/transact! tx-data {:outliner-op :save-block}))))
+                                        (set-data! (get-class-objects class)))))}))))
 
 (rum/defcs class-objects < rum/reactive db-mixins/query mixins/container-id
   [state class {:keys [current-page? sidebar?]}]
@@ -215,13 +221,16 @@
        (map (fn [row] (assoc row :id (:db/id row))))))
 
 (defn- add-new-property-object!
-  [property set-data!]
+  [property set-data! properties]
   (p/let [block (editor-handler/api-insert-new-block! ""
                                                       {:page (:block/uuid property)
-                                                       :properties {(:db/ident property) (:db/id (db/entity :logseq.property/empty-placeholder))}
+                                                       :properties (merge
+                                                                    {(:db/ident property) (:db/id (db/entity :logseq.property/empty-placeholder))}
+                                                                    properties)
                                                        :edit-block? false})
           _ (set-data! (get-property-related-objects (state/get-current-repo) property))]
-    (editor-handler/edit-block! (db/entity [:block/uuid (:block/uuid block)]) 0 :unknown-container)))
+    (editor-handler/edit-block! (db/entity [:block/uuid (:block/uuid block)]) 0 {:container-id :unknown-container})
+    block))
 
 (rum/defc property-related-objects-inner < rum/static
   [config property objects properties]
@@ -251,7 +260,8 @@
                    :set-data! set-data!
                    :title-key :views.table/property-nodes
                    :columns columns
-                   :add-new-object! #(add-new-property-object! property set-data!)
+                   :add-new-object! (fn [{:keys [properties]}]
+                                      (add-new-property-object! property set-data! properties))
                    ;; TODO: Add support for adding column
                    :show-add-property? false
                    ;; Relationships with built-in properties must not be deleted e.g. built-in? or parent

+ 1 - 1
src/main/frontend/components/property/value.cljs

@@ -968,7 +968,7 @@
             icon
             (if icon?
               (icon-component/icon icon {:color? true})
-              [:div.flex.flex-row.items-center.gap-2.h-6
+              [:div.flex.flex-row.items-center.gap-1.h-6
                (icon-component/icon icon {:color? true})
                (when value'
                  [:span value'])])

+ 2 - 2
src/main/frontend/components/repo.cljs

@@ -197,8 +197,8 @@
             :on-click (fn []
                         (file-sync/load-session-graphs)
                         (p/do!
-                          (rtc-handler/<get-remote-graphs)
-                          (repo-handler/refresh-repos!))))]]
+                         (rtc-handler/<get-remote-graphs)
+                         (repo-handler/refresh-repos!))))]]
          (repos-inner remote-graphs)])]]))
 
 (defn- check-multiple-windows?

+ 121 - 61
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.icon :as icon-component]
             [frontend.components.property.config :as property-config]
             [frontend.components.property.value :as pv]
             [frontend.components.select :as select]
@@ -18,6 +19,7 @@
             [frontend.db-mixins :as db-mixins]
             [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.property :as property-handler]
+            [frontend.handler.property.util :as pu]
             [frontend.handler.ui :as ui-handler]
             [frontend.hooks :as hooks]
             [frontend.mixins :as mixins]
@@ -28,8 +30,8 @@
             [logseq.db :as ldb]
             [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.property.type :as db-property-type]
-            [logseq.shui.table.core :as table-core]
             [logseq.shui.popup.core :as shui-popup]
+            [logseq.shui.table.core :as table-core]
             [logseq.shui.ui :as shui]
             [rum.core :as rum]))
 
@@ -43,7 +45,7 @@
       e)))
 
 (rum/defc header-checkbox < rum/static
-  [{:keys [selected-all? selected-some? toggle-selected-all!]}]
+  [{:keys [selected-all? selected-some? toggle-selected-all!] :as table}]
   (let [[show? set-show!] (rum/use-state false)]
     [:label.h-8.w-8.flex.items-center.justify-center.cursor-pointer
      {:html-for "header-checkbox"
@@ -52,7 +54,8 @@
      (shui/checkbox
       {:id "header-checkbox"
        :checked (or selected-all? (and selected-some? "indeterminate"))
-       :on-checked-change toggle-selected-all!
+       :on-checked-change (fn [value]
+                            (toggle-selected-all! table value))
        :aria-label "Select all"
        :class (str "flex transition-opacity "
                    (if (or show? selected-all? selected-some?) "opacity-100" "opacity-0"))})]))
@@ -98,47 +101,47 @@
                         [:<>
                          (when property
                            (shui/dropdown-menu-sub
-                             {:open open?
-                              :on-open-change (fn [v]
-                                                (when (or (true? v)
-                                                        (not (or (some-> @shui-popup/*last-show-target
-                                                                   (.closest "[data-icon-picker=true]"))
-                                                               (js/document.querySelector ".cp__emoji-icon-picker"))))
-                                                  (set-open! v)))}
-                             (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 {})])))
+                            {:open open?
+                             :on-open-change (fn [v]
+                                               (when (or (true? v)
+                                                         (not (or (some-> @shui-popup/*last-show-target
+                                                                          (.closest "[data-icon-picker=true]"))
+                                                                  (js/document.querySelector ".cp__emoji-icon-picker"))))
+                                                 (set-open! v)))}
+                            (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-item
-                           {:key "asc"
-                            :on-click #(column-set-sorting! sorting column true)}
-                           [:div.flex.flex-row.items-center.gap-1
-                            (ui/icon "arrow-up" {:size 15})
-                            [:div "Sort ascending"]])
+                          {:key "asc"
+                           :on-click #(column-set-sorting! sorting column true)}
+                          [:div.flex.flex-row.items-center.gap-1
+                           (ui/icon "arrow-up" {:size 15})
+                           [:div "Sort ascending"]])
                          (shui/dropdown-menu-item
-                           {:key "desc"
-                            :on-click #(column-set-sorting! sorting column false)}
-                           [:div.flex.flex-row.items-center.gap-1
-                            (ui/icon "arrow-down" {:size 15})
-                            [:div "Sort descending"]])
+                          {:key "desc"
+                           :on-click #(column-set-sorting! sorting column false)}
+                          [:div.flex.flex-row.items-center.gap-1
+                           (ui/icon "arrow-down" {:size 15})
+                           [:div "Sort descending"]])
                          (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")]]))]))]
+                            {: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"
@@ -194,20 +197,18 @@
       (reduce + (filter number? col))
       (string/join ", " col))))
 
-(rum/defcs block-container < rum/reactive db-mixins/query
-  (rum/local false ::deleted?)
+(rum/defcs block-container < (rum/local false ::deleted?)
   [state config row table]
   (let [*deleted? (::deleted? state)
-        container (state/get-component :block/container)
-        row' (db/sub-block (:db/id row))]
-    (if (nil? row')                    ; this row has been deleted
+        container (state/get-component :block/container)]
+    (if (nil? (:db/id row))                    ; this row has been deleted
       (when-not @*deleted?
         (when-let [f (get-in table [:data-fns :set-data!])]
-          (f (remove (fn [r] (= (:id r) (:id row))) (:data table)))
+          (f (remove (fn [r] (= (:id r) (:id row))) (or (:all-data table) (:data table))))
           (reset! *deleted? true)
           nil))
       [:div.relative.w-full
-       (container config row')])))
+       (container config row)])))
 
 (defn build-columns
   [config properties & {:keys [with-object-name? with-id? add-tags-column?]
@@ -318,7 +319,7 @@
     columns))
 
 (rum/defc more-actions
-  [columns {:keys [column-visible? column-toggle-visibility]}]
+  [view-entity columns {:keys [column-visible? column-toggle-visibility]}]
   (shui/dropdown-menu
    (shui/dropdown-menu-trigger
     {:asChild true}
@@ -342,6 +343,27 @@
            :checked (column-visible? column)
            :onCheckedChange #(column-toggle-visibility column %)
            :onSelect (fn [e] (.preventDefault e))}
+          (:name column)))))
+     (shui/dropdown-menu-sub
+      (shui/dropdown-menu-sub-trigger
+       "Group by")
+      (shui/dropdown-menu-sub-content
+       (for [column (filter (fn [column]
+                              (when (:id column)
+                                (when-let [p (db/entity (:id column))]
+                                  (and (not (db-property/many? p))
+                                       (contains? #{:default :number :checkbox :url :node :date}
+                                              (:logseq.property/type p)))))) columns)]
+         (shui/dropdown-menu-checkbox-item
+          {:key (str (:id column))
+           :className "capitalize"
+           :checked (= (:id column) (:db/ident (:logseq.property.view/group-by-property view-entity)))
+           :onCheckedChange (fn [result]
+                              (if result
+                                (db-property-handler/set-block-property! (:db/id view-entity) :logseq.property.view/group-by-property
+                                                                         (:db/id (db/entity (:id column))))
+                                (db-property-handler/remove-block-property! (:db/id view-entity) :logseq.property.view/group-by-property)))
+           :onSelect (fn [e] (.preventDefault e))}
           (:name column)))))))))
 
 (defn- get-column-size
@@ -581,7 +603,7 @@
      [:div.flex.flex-row
       (map-indexed row-cell-f unpinned-columns)])))
 
-(rum/defc table-row < rum/reactive
+(rum/defc table-row < rum/reactive db-mixins/query
   [table row props option]
   (let [row' (db/sub-block (:id row))
         ;; merge entity temporal attributes
@@ -1198,7 +1220,7 @@
        (property-handler/set-block-property! repo (:db/id entity) :logseq.property.table/sized-columns sized-columns))}))
 
 (rum/defc table-view < rum/static
-  [table option row-selection add-new-object! *scroller-ref]
+  [table option row-selection *scroller-ref]
   (let [selected-rows (shui/table-get-selection-rows row-selection (:rows table))
         [ready? set-ready?] (rum/use-state false)
         *rows-wrap (rum/use-ref nil)]
@@ -1228,7 +1250,7 @@
                              (let [row (nth rows idx)]
                                (table-row table row {} option)))})
 
-           (when add-new-object!
+           (when (get-in table [:data-fns :add-new-object!])
              (shui/table-footer (add-new-row table)))])]))))
 
 (rum/defc list-view < rum/static
@@ -1380,6 +1402,17 @@
                                   {:align :end}))}
    (ui/icon "arrows-up-down")))
 
+(defn- view-cp
+  [view-entity table option {:keys [*scroller-ref display-type row-selection]}]
+  (case display-type
+    :logseq.property.view/type.list
+    (list-view (:config option) view-entity (:rows table))
+
+    :logseq.property.view/type.gallery
+    (gallery-view (:config option) table view-entity (:rows table) *scroller-ref)
+
+    (table-view table option row-selection *scroller-ref)))
+
 (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}}
@@ -1429,6 +1462,7 @@
                                                (remove (fn [column]
                                                          (false? (get visible-columns (:id column))))
                                                        columns))
+        group-by-property (:logseq.property.view/group-by-property view-entity)
         table-map {:view-entity view-entity
                    :data data
                    :columns columns
@@ -1440,7 +1474,8 @@
                            :sized-columns sized-columns
                            :ordered-columns ordered-columns
                            :pinned-columns pinned
-                           :unpinned-columns unpinned}
+                           :unpinned-columns unpinned
+                           :group-by-property group-by-property}
                    :data-fns {:set-data! set-data!
                               :set-row-filter! set-row-filter!
                               :set-filters! set-filters!
@@ -1482,20 +1517,45 @@
         [:div.text-muted-foreground.text-sm
          (pv/property-value view-entity (db/entity :logseq.property.view/type) {})]
 
-        (more-actions columns table)
+        (more-actions view-entity columns table)
 
         (when add-new-object! (new-record-button table view-entity))]]
       [:div.ls-view-body.flex.flex-col.gap-2.grid
        (filters-row table)
 
-       (case display-type
-         :logseq.property.view/type.list
-         (list-view (:config option) view-entity (:rows table))
-
-         :logseq.property.view/type.gallery
-         (gallery-view (:config option) table view-entity (:rows table) *scroller-ref)
-
-         (table-view table option row-selection add-new-object! *scroller-ref))]
+       (let [view-opts {:*scroller-ref *scroller-ref
+                        :display-type display-type
+                        :row-selection row-selection
+                        :add-new-object! add-new-object!}]
+         (if group-by-property
+           (let [readable-property-value #(if (de/entity? %) (db-property/property-value-content %) (str %))
+                 ;; similar to readable-property but return entity if :db/ident to allow for icons
+                 groupable-readable-property-value #(if (de/entity? %)
+                                                      (if (:db/ident %) % (db-property/property-value-content %))
+                                                      (str %))
+                 groups (->> (group-by #(-> (:db/ident group-by-property) % groupable-readable-property-value)
+                                       (:rows table))
+                             (sort-by #(db-property/property-value-content (first %))))]
+             [:div.flex.flex-col.gap-4.border-t.py-4
+              (for [[value group] groups]
+                (let [add-new-object! (fn [_]
+                                        (add-new-object! {:properties {(:db/ident group-by-property) (or (and (map? value) (:db/id value)) value)}}))
+                      table' (shui/table-option (-> table-map
+                                                    (assoc-in [:data-fns :add-new-object!] add-new-object!)
+                                                    (assoc :data group
+                                                           :all-data (:data table))))]
+                  (ui/foldable
+                   [:div.text-sm.font-medium.ml-2
+                    (if (some? value)
+                      (let [icon (pu/get-block-property-value value :logseq.property/icon)]
+                        [:div.flex.flex-row.gap-1.items-center
+                         (when icon (icon-component/icon icon {:color? true}))
+                         (readable-property-value value)])
+                      (str "No " (:block/title group-by-property)))]
+                   [:div.mt-2
+                    (view-cp view-entity (assoc table' :rows group) option view-opts)]
+                   {:title-trigger? false})))])
+           (view-cp view-entity table option view-opts)))]
       {:title-trigger? false})]))
 
 (rum/defcs view

+ 1 - 1
src/main/frontend/handler/db_based/export.cljs

@@ -4,10 +4,10 @@
             [clojure.edn :as edn]
             [frontend.db :as db]
             [frontend.handler.notification :as notification]
+            [frontend.handler.ui :as ui-handler]
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util.page :as page-util]
-            [frontend.handler.ui :as ui-handler]
             [logseq.db :as ldb]
             [logseq.db.sqlite.export :as sqlite-export]
             [logseq.shui.ui :as shui]

+ 1 - 1
src/main/frontend/handler/events.cljs

@@ -414,7 +414,7 @@
 
 (defmethod handle :go/install-plugin-from-github [[_]]
   (shui/dialog-open!
-    (plugin/install-from-github-release-container)))
+   (plugin/install-from-github-release-container)))
 
 (defmethod handle :go/plugins-settings [[_ pid nav? title]]
   (when pid

+ 12 - 12
src/main/frontend/handler/repo.cljs

@@ -1,31 +1,31 @@
 (ns frontend.handler.repo
   "System-component-like ns that manages user's repos/graphs"
   (:refer-clojure :exclude [clone])
-  (:require [clojure.string :as string]
+  (:require [borkdude.rewrite-edn :as rewrite]
+            [cljs-bean.core :as bean]
+            [clojure.string :as string]
+            [electron.ipc :as ipc]
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.db :as db]
+            [frontend.db.persist :as db-persist]
             [frontend.db.restore :as db-restore]
-            [frontend.handler.repo-config :as repo-config-handler]
             [frontend.handler.common.config-edn :as config-edn-common-handler]
-            [frontend.handler.route :as route-handler]
-            [frontend.handler.ui :as ui-handler]
-            [frontend.handler.notification :as notification]
             [frontend.handler.global-config :as global-config-handler]
             [frontend.handler.graph :as graph-handler]
+            [frontend.handler.notification :as notification]
+            [frontend.handler.repo-config :as repo-config-handler]
+            [frontend.handler.route :as route-handler]
+            [frontend.handler.ui :as ui-handler]
             [frontend.idb :as idb]
+            [frontend.mobile.util :as mobile-util]
+            [frontend.persist-db :as persist-db]
             [frontend.search :as search]
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.util.fs :as util-fs]
             [frontend.util.text :as text-util]
-            [frontend.persist-db :as persist-db]
-            [promesa.core :as p]
-            [frontend.db.persist :as db-persist]
-            [electron.ipc :as ipc]
-            [cljs-bean.core :as bean]
-            [frontend.mobile.util :as mobile-util]
-            [borkdude.rewrite-edn :as rewrite]))
+            [promesa.core :as p]))
 
 ;; Project settings should be checked in two situations:
 ;; 1. User changes the config.edn directly in logseq.com (fn: alter-file)

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

@@ -602,6 +602,15 @@
     (d/reset-schema! conn (dissoc schema :block/schema))
     []))
 
+(defn- add-view-icons
+  [_conn _search-db]
+  [{:db/ident :logseq.property.view/type.table
+    :logseq.property/icon {:type :tabler-icon :id "table"}}
+   {:db/ident :logseq.property.view/type.list
+    :logseq.property/icon {:type :tabler-icon :id "list"}}
+   {:db/ident :logseq.property.view/type.gallery
+    :logseq.property/icon {:type :tabler-icon :id "layout-grid"}}])
+
 (def ^:large-vars/cleanup-todo schema-version->updates
   "A vec of tuples defining datascript migrations. Each tuple consists of the
    schema version integer and a migration map. A migration map can have keys of :properties, :classes
@@ -703,7 +712,8 @@
    [64 {:fix update-view-filter}]
    ;;;; schema-version format: "<major>.<minor>"
    ;;;; int number equals to "<major>" (without <minor>)
-   ])
+   ["64.1" {:properties [:logseq.property.view/group-by-property]
+            :fix add-view-icons}]])
 
 (let [max-schema-version (last (sort (map (comp (juxt :major :minor) db-schema/parse-schema-version first)
                                           schema-version->updates)))

+ 1 - 1
src/main/frontend/worker/rtc/asset.cljs

@@ -7,11 +7,11 @@
     indicates need to upload the asset to server"
   (:require [clojure.set :as set]
             [datascript.core :as d]
+            [frontend.common.missionary :as c.m]
             [frontend.worker.rtc.client-op :as client-op]
             [frontend.worker.rtc.log-and-state :as rtc-log-and-state]
             [frontend.worker.rtc.ws-util :as ws-util]
             [frontend.worker.state :as worker-state]
-            [frontend.common.missionary :as c.m]
             [logseq.common.path :as path]
             [logseq.db :as ldb]
             [malli.core :as ma]

+ 1 - 1
src/main/frontend/worker/rtc/exception.cljs

@@ -14,7 +14,7 @@ It's a server internal error, shouldn't happen."}
 Trying to start rtc loop but there's already one running, need to cancel that one first."}
   :rtc.exception/not-found-db-conn {:doc "Local exception. Cannot find db-conn by repo"}
   :rtc.exception/not-found-schema-version {:doc "Local exception. graph doesn't have :logseq.kv/schema-version value"}
-  :rtc.exception/not-found-remote-schema-version{:doc "Local exception.
+  :rtc.exception/not-found-remote-schema-version {:doc "Local exception.
 graph doesn't have :logseq.kv/remote-schema-version value"}
   :rtc.exception/major-schema-version-mismatched {:doc "Local exception.
 local-schema-version, remote-schema-version, app-schema-version are not equal, cannot start rtc"}

+ 1 - 1
src/rtc_e2e_test/fixture.cljs

@@ -3,11 +3,11 @@
             [const]
             [datascript.core :as d]
             [example]
+            [frontend.common.missionary :as c.m]
             [frontend.worker.rtc.client-op :as client-op]
             [frontend.worker.rtc.db-listener]
             [frontend.worker.state :as worker-state]
             [helper]
-            [frontend.common.missionary :as c.m]
             [missionary.core :as m]))
 
 (def graph-schema-version "0")

+ 4 - 4
src/rtc_e2e_test/helper.cljs

@@ -94,11 +94,11 @@
   #_:clj-kondo/ignore
   (me/find
    client-op
-    [?op-type _ {:block-uuid ?block-uuid :av-coll [[!a !v _ !add] ...]}]
-    [?op-type ?block-uuid (map vector !a !v !add)]
+   [?op-type _ {:block-uuid ?block-uuid :av-coll [[!a !v _ !add] ...]}]
+   [?op-type ?block-uuid (map vector !a !v !add)]
 
-    [?op-type _ {:block-uuid ?block-uuid}]
-    [?op-type ?block-uuid]))
+   [?op-type _ {:block-uuid ?block-uuid}]
+   [?op-type ?block-uuid]))
 
 (defn new-task--wait-all-client-ops-sent
   [& {:keys [timeout] :or {timeout 10000}}]